+// NOTE: We use eslint now. This file is used only by shifter. We keep the configuration
+// here because shifter uses jshint after modules have been concating. Eslint can't
+// currently do this.
{
"asi": false,
"bitwise": true,
php:
# We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
- 7.0
- # - 5.6
- # - 5.5
- - 5.4
+ - 5.6
env:
# Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
# start first so that the total run time is not too high.
#
- # We only run MySQL on PHP 5.6, so run that first.
+ # We only run MySQL on PHP 7.0, so run that first.
# CI Tests should be second-highest in priority as these only take <= 60 seconds to run under normal circumstances.
# Postgres is significantly is pretty reasonable in its run-time.
exclude:
# MySQL - it's just too slow.
# Exclude it on all versions except for 7.0
- # - env: DB=mysqli TASK=PHPUNIT
- # php: 5.6
- #
- # - env: DB=mysqli TASK=PHPUNIT
- # php: 5.5
- env: DB=mysqli TASK=PHPUNIT
- php: 5.4
+ php: 5.6
+ # One grunt execution is enough.
- env: DB=none TASK=GRUNT
- php: 5.4
+ php: 5.6
# Moodle 2.7 is not compatible with PHP 7 for the upgrade test.
- env: DB=pgsql TASK=UPGRADE
// Project configuration.
grunt.initConfig({
- jshint: {
- options: {jshintrc: '.jshintrc'},
- amd: { src: amdSrc }
- },
eslint: {
// Even though warnings dont stop the build we don't display warnings by default because
// at this moment we've got too many core warnings.
var changedFiles = Object.create(null);
var onChange = grunt.util._.debounce(function() {
var files = Object.keys(changedFiles);
- grunt.config('jshint.amd.src', files);
+ grunt.config('eslint.amd.src', files);
+ grunt.config('eslint.yui.src', files);
grunt.config('uglify.amd.files', [{ expand: true, src: files, rename: uglifyRename }]);
grunt.config('shifter.options.paths', files);
changedFiles = Object.create(null);
// Register NPM tasks.
grunt.loadNpmTasks('grunt-contrib-uglify');
- grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-eslint');
grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles);
grunt.registerTask('yui', ['eslint:yui', 'shifter']);
- grunt.registerTask('amd', ['eslint:amd', 'jshint', 'uglify']);
+ grunt.registerTask('amd', ['eslint:amd', 'uglify']);
grunt.registerTask('js', ['amd', 'yui']);
// Register CSS taks.
define('IGNORE_COMPONENT_CACHE', true);
// Check that PHP is of a sufficient version
-if (version_compare(phpversion(), "5.4.4") < 0) {
+if (version_compare(phpversion(), "5.6.5") < 0) {
$phpversion = phpversion();
// do NOT localise - lang strings would not work here and we CAN NOT move it after installib
- fwrite(STDERR, "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).\n");
+ fwrite(STDERR, "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n");
fwrite(STDERR, "Please upgrade your server software or install older Moodle version.\n");
exit(1);
}
";
// Check that PHP is of a sufficient version
-if (version_compare(phpversion(), "5.4.4") < 0) {
+if (version_compare(phpversion(), "5.6.5") < 0) {
$phpversion = phpversion();
// do NOT localise - lang strings would not work here and we CAN NOT move it after installib
- fwrite(STDERR, "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).\n");
+ fwrite(STDERR, "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n");
fwrite(STDERR, "Please upgrade your server software or install older Moodle version.\n");
exit(1);
}
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
+ <MOODLE version="3.2" requires="2.7">
+ <UNICODE level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unicoderequired" />
+ </FEEDBACK>
+ </UNICODE>
+ <DATABASE level="required">
+ <VENDOR name="mariadb" version="5.5.31" />
+ <VENDOR name="mysql" version="5.5.31" />
+ <VENDOR name="postgres" version="9.1" />
+ <VENDOR name="mssql" version="10.0" />
+ <VENDOR name="oracle" version="10.2" />
+ </DATABASE>
+ <PHP version="5.6.5" level="required">
+ </PHP>
+ <PCREUNICODE level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="pcreunicodewarning" />
+ </FEEDBACK>
+ </PCREUNICODE>
+ <PHP_EXTENSIONS>
+ <PHP_EXTENSION name="iconv" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="iconvrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="mbstring" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="mbstringrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="curl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="curlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="openssl" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opensslrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="tokenizer" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="tokenizerrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xmlrpc" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="xmlrpcrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="soap" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="soaprecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="ctype" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ctyperequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zip" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ziprequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zlib" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="gd" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="gdrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="simplexml" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="simplexmlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="spl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="splrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="pcre" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="dom" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xml" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xmlreader" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="intl" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="intlrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="json" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="hash" level="required"/>
+ </PHP_EXTENSIONS>
+ <PHP_SETTINGS>
+ <PHP_SETTING name="memory_limit" value="96M" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="settingmemorylimit" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="file_uploads" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="settingfileuploads" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="opcache.enable" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opcacherecommended" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ </PHP_SETTINGS>
+ <CUSTOM_CHECKS>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unsupporteddbstorageengine" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="quizattemptsupgradedmessage" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="slashargumentswarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="unsupporteddbtablerowformat" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="unoconvwarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ </CUSTOM_CHECKS>
+ </MOODLE>
</COMPATIBILITY_MATRIX>
}
// Check that PHP is of a sufficient version as soon as possible
-if (version_compare(phpversion(), '5.4.4') < 0) {
+if (version_compare(phpversion(), '5.6.5') < 0) {
$phpversion = phpversion();
// do NOT localise - lang strings would not work here and we CAN NOT move it to later place
- echo "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).<br />";
+ echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).<br />";
echo "Please upgrade your server software or install older Moodle version.";
die();
}
require_capability('moodle/role:review', $context);
require_sesskey();
+$OUTPUT->header();
+
list($overridableroles, $overridecounts, $nameswithcounts) = get_overridable_roles($context,
ROLENAME_BOTH, true);
list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course);
$output = $PAGE->get_renderer('tool_lp');
+$page = new \tool_lp\output\course_competencies_page($course->id);
+
echo $output->header();
echo $output->heading($title);
-
-$page = new \tool_lp\output\course_competencies_page($course->id);
echo $output->render($page);
echo $output->footer();
return;
}
+ // Check access to the course and competencies page.
+ $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
+ $context = context_course::instance($course->id);
+ if (!has_any_capability($capabilities, $context) || !can_access_course($course)) {
+ return;
+ }
+
// Just a link to course competency.
$title = get_string('competencies', 'core_competency');
$path = new moodle_url("/admin/tool/lp/coursecompetencies.php", array('courseid' => $course->id));
use core_component;
use core_plugin_manager;
+use context_system;
/**
* API exposed by tool_mobile
return $pluginsinfo;
}
+ /**
+ * Returns a list of the site public settings, those not requiring authentication.
+ *
+ * @return array with the settings and warnings
+ */
+ public static function get_site_public_settings() {
+ global $CFG, $SITE, $PAGE;
+
+ $context = context_system::instance();
+ // We need this to make work the format text functions.
+ $PAGE->set_context($context);
+
+ $settings = array(
+ 'wwwroot' => $CFG->wwwroot,
+ 'httpswwwroot' => $CFG->httpswwwroot,
+ 'sitename' => external_format_string($SITE->fullname, $context->id, true),
+ 'guestlogin' => $CFG->guestloginbutton,
+ 'rememberusername' => $CFG->rememberusername,
+ 'authloginviaemail' => $CFG->authloginviaemail,
+ 'registerauth' => $CFG->registerauth,
+ 'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
+ 'authinstructions' => format_text($CFG->auth_instructions),
+ 'authnoneenabled' => (int) is_enabled_auth('none'),
+ 'enablewebservices' => $CFG->enablewebservices,
+ 'enablemobilewebservice' => $CFG->enablemobilewebservice,
+ 'maintenanceenabled' => $CFG->maintenance_enabled,
+ 'maintenancemessage' => format_text($CFG->maintenance_message),
+ );
+ return $settings;
+ }
+
}
);
}
+ /**
+ * Returns description of get_site_public_settings() parameters.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.2
+ */
+ public static function get_site_public_settings_parameters() {
+ return new external_function_parameters(array());
+ }
+
+ /**
+ * Returns a list of the site public settings, those not requiring authentication.
+ *
+ * @return array with the settings and warnings
+ * @since Moodle 3.2
+ */
+ public static function get_site_public_settings() {
+ $result = api::get_site_public_settings();
+ $result['warnings'] = array();
+ return $result;
+ }
+
+ /**
+ * Returns description of get_site_public_settings() result value.
+ *
+ * @return external_description
+ * @since Moodle 3.2
+ */
+ public static function get_site_public_settings_returns() {
+ return new external_single_structure(
+ array(
+ 'wwwroot' => new external_value(PARAM_RAW, 'Site URL.'),
+ 'httpswwwroot' => new external_value(PARAM_RAW, 'Site https URL (if httpslogin is enabled).'),
+ 'sitename' => new external_value(PARAM_TEXT, 'Site name.'),
+ 'guestlogin' => new external_value(PARAM_INT, 'Whether guest login is enabled.'),
+ 'rememberusername' => new external_value(PARAM_INT, 'Values: 0 for No, 1 for Yes, 2 for optional.'),
+ 'authloginviaemail' => new external_value(PARAM_INT, 'Whether log in via email is enabled.'),
+ 'registerauth' => new external_value(PARAM_PLUGIN, 'Authentication method for user registration.'),
+ 'forgottenpasswordurl' => new external_value(PARAM_URL, 'Forgotten password URL.'),
+ 'authinstructions' => new external_value(PARAM_RAW, 'Authentication instructions.'),
+ 'authnoneenabled' => new external_value(PARAM_INT, 'Whether auth none is enabled.'),
+ 'enablewebservices' => new external_value(PARAM_INT, 'Whether Web Services are enabled.'),
+ 'enablemobilewebservice' => new external_value(PARAM_INT, 'Whether the Mobile service is enabled.'),
+ 'maintenanceenabled' => new external_value(PARAM_INT, 'Whether site maintenance is enabled.'),
+ 'maintenancemessage' => new external_value(PARAM_RAW, 'Maintenance message.'),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
}
'description' => 'Returns a list of Moodle plugins supporting the mobile app.',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ ),
+
+ 'tool_mobile_get_site_public_settings' => array(
+ 'classname' => 'tool_mobile\external',
+ 'methodname' => 'get_site_public_settings',
+ 'description' => 'Returns a list of the site public settings, those not requiring authentication.',
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+ 'ajax' => true,
+ 'loginrequired' => false,
)
);
$this->assertTrue(is_array($result['plugins']));
}
+ public function test_get_site_public_settings() {
+ global $CFG, $SITE;
+
+ $this->resetAfterTest(true);
+ $result = external::get_site_public_settings();
+ $result = external_api::clean_returnvalue(external::get_site_public_settings_returns(), $result);
+
+ // Test default values.
+ $context = context_system::instance();
+ $expected = array(
+ 'wwwroot' => $CFG->wwwroot,
+ 'httpswwwroot' => $CFG->httpswwwroot,
+ 'sitename' => external_format_string($SITE->fullname, $context->id, true),
+ 'guestlogin' => $CFG->guestloginbutton,
+ 'rememberusername' => $CFG->rememberusername,
+ 'authloginviaemail' => $CFG->authloginviaemail,
+ 'registerauth' => $CFG->registerauth,
+ 'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
+ 'authinstructions' => format_text($CFG->auth_instructions),
+ 'authnoneenabled' => (int) is_enabled_auth('none'),
+ 'enablewebservices' => $CFG->enablewebservices,
+ 'enablemobilewebservice' => $CFG->enablemobilewebservice,
+ 'maintenanceenabled' => $CFG->maintenance_enabled,
+ 'maintenancemessage' => format_text($CFG->maintenance_message),
+ 'warnings' => array()
+ );
+ $this->assertEquals($expected, $result);
+
+ // Change a value.
+ set_config('registerauth', 'email');
+ $authinstructions = 'Something with <b>html tags</b>';
+ set_config('auth_instructions', $authinstructions);
+
+ $expected['registerauth'] = 'email';
+ $expected['authinstructions'] = format_text($authinstructions);
+
+ $result = external::get_site_public_settings();
+ $result = external_api::clean_returnvalue(external::get_site_public_settings_returns(), $result);
+ $this->assertEquals($expected, $result);
+ }
+
}
*/
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2016052301; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2016051900; // Requires this Moodle version.
$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
$subscriptions = subscription_manager::get_subscriptions_by_event($eventobj);
$idstosend = array();
foreach ($subscriptions as $subscription) {
+ // Only proceed to fire events and notifications if the subscription is active.
+ if (!subscription_manager::subscription_is_active($subscription)) {
+ continue;
+ }
$starttime = $now - $subscription->timewindow;
$starttime = ($starttime > $subscription->lastnotificationsent) ? $starttime : $subscription->lastnotificationsent;
if ($subscription->courseid == 0) {
* Magic get method.
*
* @param string $prop property to get.
- *
* @return mixed
* @throws \coding_exception
*/
public function __get($prop) {
- if (property_exists($this->subscription, $prop)) {
+ if (isset($this->subscription->$prop)) {
return $this->subscription->$prop;
}
throw new \coding_exception('Property "' . $prop . '" doesn\'t exist');
}
+ /**
+ * Magic isset method.
+ *
+ * @param string $prop the property to get.
+ * @return bool true if the property is set, false otherwise.
+ */
+ public function __isset($prop) {
+ return property_exists($this->subscription, $prop);
+ }
+
/**
* Get a human readable name for instances associated with this subscription.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class subscription_manager {
+
+ /** @const Period of time, in days, after which an inactive subscription will be removed completely.*/
+ const INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS = 30;
+
/**
* Subscribe a user to a given rule.
*
return false;
}
+
+ /**
+ * Activates a group of subscriptions based on an input array of ids.
+ *
+ * @since 3.2.0
+ * @param array $ids of subscription ids.
+ * @return bool true if the operation was successful, false otherwise.
+ */
+ public static function activate_subscriptions(array $ids) {
+ global $DB;
+ if (!empty($ids)) {
+ list($sql, $params) = $DB->get_in_or_equal($ids);
+ $success = $DB->set_field_select('tool_monitor_subscriptions', 'inactivedate', '0', 'id ' . $sql, $params);
+ return $success;
+ }
+ return false;
+ }
+
+ /**
+ * Deactivates a group of subscriptions based on an input array of ids.
+ *
+ * @since 3.2.0
+ * @param array $ids of subscription ids.
+ * @return bool true if the operation was successful, false otherwise.
+ */
+ public static function deactivate_subscriptions(array $ids) {
+ global $DB;
+ if (!empty($ids)) {
+ $inactivedate = time();
+ list($sql, $params) = $DB->get_in_or_equal($ids);
+ $success = $DB->set_field_select('tool_monitor_subscriptions', 'inactivedate', $inactivedate, 'id ' . $sql,
+ $params);
+ return $success;
+ }
+ return false;
+ }
+
+ /**
+ * Deletes subscriptions which have been inactive for a period of time.
+ *
+ * @since 3.2.0
+ * @param int $userid if provided, only this user's stale subscriptions will be deleted.
+ * @return bool true if the operation was successful, false otherwise.
+ */
+ public static function delete_stale_subscriptions($userid = 0) {
+ global $DB;
+ // Get the expiry duration, in days.
+ $cutofftime = strtotime("-" . self::INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS . " days", time());
+
+ if (!empty($userid)) {
+ // Remove any stale subscriptions for the desired user only.
+ $success = $DB->delete_records_select('tool_monitor_subscriptions',
+ 'userid = ? AND inactivedate < ? AND inactivedate <> 0',
+ array($userid, $cutofftime));
+
+ } else {
+ // Remove all stale subscriptions.
+ $success = $DB->delete_records_select('tool_monitor_subscriptions',
+ 'inactivedate < ? AND inactivedate <> 0',
+ array($cutofftime));
+ }
+ return $success;
+ }
+
+ /**
+ * Check whether a subscription is active.
+ *
+ * @since 3.2.0
+ * @param \tool_monitor\subscription $subscription instance.
+ * @return bool true if the subscription is active, false otherwise.
+ */
+ public static function subscription_is_active(subscription $subscription) {
+ return empty($subscription->inactivedate);
+ }
}
--- /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/>.
+namespace tool_monitor\task;
+use tool_monitor\subscription;
+use tool_monitor\subscription_manager;
+
+/**
+ * Simple task class responsible for activating, deactivating and removing subscriptions.
+ *
+ * Activation/deactivation is managed by looking at the same access rules used to determine whether a user can
+ * subscribe to the rule in the first place.
+ *
+ * Removal occurs when a subscription has been inactive for a period of time exceeding the lifespan, as set by
+ * subscription_manager::get_inactive_subscription_lifespan().
+ *
+ * I.e.
+ * - Activation: If a user can subscribe currently, then an existing subscription should be made active.
+ * - Deactivation: If a user cannot subscribe currently, then an existing subscription should be made inactive.
+ * - Removal: If a user has a subscription that has been inactive for longer than the prescribed period, then
+ * delete the subscription entirely.
+ *
+ * @since 3.2.0
+ * @package tool_monitor
+ * @copyright 2016 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class check_subscriptions extends \core\task\scheduled_task {
+
+ /** @var array 1d static cache, indexed by userid, storing whether or not the user has been fully set up.*/
+ protected $userssetupcache = array();
+
+ /** @var array 2d static cache, indexed by courseid and userid, storing whether a user can access the course with
+ * the 'tool/monitor:subscribe' capability.
+ */
+ protected $courseaccesscache = array();
+
+ /**
+ * Get a descriptive name for this task.
+ *
+ * @since 3.2.0
+ * @return string name of the task.
+ */
+ public function get_name() {
+ return get_string('taskchecksubscriptions', 'tool_monitor');
+ }
+
+ /**
+ * Checks all course-level rule subscriptions and activates/deactivates based on current course access.
+ *
+ * The ordering of checks within the task is important for optimisation purposes. The aim is to be able to make a decision
+ * about whether to activate/deactivate each subscription without making unnecessary checks. The ordering roughly follows the
+ * context model, starting with system and user checks and moving down to course and course-module only when necessary.
+ *
+ * For example, if the user is suspended, then any active subscription is made inactive right away. I.e. there is no need to
+ * check site-level, course-level or course-module-level permissions. Likewise, if a subscriptions is site-level, there is no
+ * need to check course-level and course-module-level permissions.
+ *
+ * The task performs the following checks, in this order:
+ * 1. Check for a suspended user, breaking if suspended.
+ * 2. Check for an incomplete (not set up) user, breaking if not fully set up.
+ * 3. Check for the required capability in the relevant context, breaking if the capability is not found.
+ * 4. Check whether the subscription is site-context, breaking if true.
+ * 5. Check whether the user has course access, breaking only if the subscription is not also course-module-level.
+ * 6. Check whether the user has course-module access.
+ *
+ * @since 3.2.0
+ */
+ public function execute() {
+ global $DB;
+
+ if (!get_config('tool_monitor', 'enablemonitor')) {
+ return; // The tool is disabled. Nothing to do.
+ }
+
+ $toactivate = array(); // Store the ids of subscriptions to be activated upon completion.
+ $todeactivate = array(); // Store the ids of subscriptions to be deactivated upon completion.
+
+ // Resultset rows are ordered by userid and courseid to work nicely with get_fast_modinfo() caching.
+ $sql = "SELECT u.id AS userid, u.firstname AS userfirstname, u.lastname AS userlastname, u.suspended AS usersuspended,
+ u.email AS useremail, c.visible as coursevisible, c.cacherev as coursecacherev, s.courseid AS subcourseid,
+ s.userid AS subuserid, s.cmid AS subcmid, s.inactivedate AS subinactivedate, s.id AS subid
+ FROM {user} u
+ JOIN {tool_monitor_subscriptions} s ON (s.userid = u.id)
+ LEFT JOIN {course} c ON (c.id = s.courseid)
+ WHERE u.id = s.userid
+ ORDER BY s.userid, s.courseid";
+ $rs = $DB->get_recordset_sql($sql);
+
+ foreach ($rs as $row) {
+ // Create skeleton records from the result. This should be enough to use in subsequent access calls and avoids DB hits.
+ $sub = $this->get_subscription_from_rowdata($row);
+ $sub = new subscription($sub);
+ if (!isset($user) || $user->id != $sub->userid) {
+ $user= $this->get_user_from_rowdata($row);
+ }
+ if ((!isset($course) || $course->id != $sub->courseid) && !empty($sub->courseid)) {
+ $course = $this->get_course_from_rowdata($row);
+ }
+
+ // The user is suspended at site level, so deactivate any active subscriptions.
+ if ($user->suspended) {
+ if (subscription_manager::subscription_is_active($sub)) {
+ $todeactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // Is the user fully set up? As per require_login on the subscriptions page.
+ if (!$this->is_user_setup($user)) {
+ if (subscription_manager::subscription_is_active($sub)) {
+ $todeactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // Determine the context, based on the subscription course id.
+ $sitelevelsubscription = false;
+ if (empty($sub->courseid)) {
+ $context = \context_system::instance();
+ $sitelevelsubscription = true;
+ } else {
+ $context = \context_course::instance($sub->courseid);
+ }
+
+ // Check capability in the context.
+ if (!has_capability('tool/monitor:subscribe', $context, $user)) {
+ if (subscription_manager::subscription_is_active($sub)) {
+ $todeactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // If the subscription is site-level, then we've run all the checks required to make an access decision.
+ if ($sitelevelsubscription) {
+ if (!subscription_manager::subscription_is_active($sub)) {
+ $toactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // Check course access.
+ if (!$this->user_can_access_course($user, $course, 'tool/monitor:subscribe')) {
+ if (subscription_manager::subscription_is_active($sub)) {
+ $todeactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // If the subscription has no course module relationship.
+ if (empty($sub->cmid)) {
+ if (!subscription_manager::subscription_is_active($sub)) {
+ $toactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // Otherwise, check the course module info. We use the same checks as on the subscription page.
+ $modinfo = get_fast_modinfo($course, $sub->userid);
+ $cm = $modinfo->get_cm($sub->cmid);
+ if (!$cm || !$cm->uservisible || !$cm->available) {
+ if (subscription_manager::subscription_is_active($sub)) {
+ $todeactivate[] = $sub->id;
+ }
+ continue;
+ }
+
+ // The course module is available and visible, so make a decision.
+ if (!subscription_manager::subscription_is_active($sub)) {
+ $toactivate[] = $sub->id;
+ }
+ }
+ $rs->close();
+
+ // Activate/deactivate/delete relevant subscriptions.
+ subscription_manager::activate_subscriptions($toactivate);
+ subscription_manager::deactivate_subscriptions($todeactivate);
+ subscription_manager::delete_stale_subscriptions();
+ }
+
+ /**
+ * Determines whether a user is fully set up, using cached results where possible.
+ *
+ * @since 3.2.0
+ * @param \stdClass $user the user record.
+ * @return bool true if the user is fully set up, false otherwise.
+ */
+ protected function is_user_setup($user) {
+ if (!isset($this->userssetupcache[$user->id])) {
+ $this->userssetupcache[$user->id] = !user_not_fully_set_up($user);
+ }
+ return $this->userssetupcache[$user->id];
+ }
+
+ /**
+ * Determines a user's access to a course with a given capability, using cached results where possible.
+ *
+ * @since 3.2.0
+ * @param \stdClass $user the user record.
+ * @param \stdClass $course the course record.
+ * @param string $capability the capability to check.
+ * @return bool true if the user can access the course with the specified capability, false otherwise.
+ */
+ protected function user_can_access_course($user, $course, $capability) {
+ if (!isset($this->courseaccesscache[$course->id][$user->id][$capability])) {
+ $this->courseaccesscache[$course->id][$user->id][$capability] = can_access_course($course, $user, $capability, true);
+ }
+ return $this->courseaccesscache[$course->id][$user->id][$capability];
+ }
+
+ /**
+ * Returns a partial subscription record, created from properties of the supplied recordset row object.
+ * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
+ *
+ * @since 3.2.0
+ * @param \stdClass $rowdata the row object.
+ * @return \stdClass a partial subscription record.
+ */
+ protected function get_subscription_from_rowdata($rowdata) {
+ $sub = new \stdClass();
+ $sub->id = $rowdata->subid;
+ $sub->userid = $rowdata->subuserid;
+ $sub->courseid = $rowdata->subcourseid;
+ $sub->cmid = $rowdata->subcmid;
+ $sub->inactivedate = $rowdata->subinactivedate;
+ return $sub;
+ }
+
+ /**
+ * Returns a partial course record, created from properties of the supplied recordset row object.
+ * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
+ *
+ * @since 3.2.0
+ * @param \stdClass $rowdata the row object.
+ * @return \stdClass a partial course record.
+ */
+ protected function get_course_from_rowdata($rowdata) {
+ $course = new \stdClass();
+ $course->id = $rowdata->subcourseid;
+ $course->visible = $rowdata->coursevisible;
+ $course->cacherev = $rowdata->coursecacherev;
+ return $course;
+ }
+
+ /**
+ * Returns a partial user record, created from properties of the supplied recordset row object.
+ * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
+ *
+ * @since 3.2.0
+ * @param \stdClass $rowdata the row object.
+ * @return \stdClass a partial user record.
+ */
+ protected function get_user_from_rowdata($rowdata) {
+ $user = new \stdClass();
+ $user->id = $rowdata->userid;
+ $user->firstname = $rowdata->userfirstname;
+ $user->lastname = $rowdata->userlastname;
+ $user->email = $rowdata->useremail;
+ $user->suspended = $rowdata->usersuspended;
+ return $user;
+ }
+}
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User id of the subscriber"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when this subscription was created"/>
<FIELD NAME="lastnotificationsent" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of the time when a notification was last sent for this subscription."/>
+ <FIELD NAME="inactivedate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
'day' => '*',
'dayofweek' => '*',
'month' => '*'
+ ),
+ array(
+ 'classname' => 'tool_monitor\task\check_subscriptions',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => 'R',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
)
);
// Moodle v3.1.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2016052305) {
+
+ // Define field inactivedate to be added to tool_monitor_subscriptions.
+ $table = new xmldb_table('tool_monitor_subscriptions');
+ $field = new xmldb_field('inactivedate', XMLDB_TYPE_INTEGER, '10', null, true, null, 0, 'lastnotificationsent');
+
+ // Conditionally launch add field inactivedate.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Monitor savepoint reached.
+ upgrade_plugin_savepoint(true, 2016052305, 'tool', 'monitor');
+ }
+
return true;
}
$string['subhelp_help'] = 'This subscription listens for when the event \'{$a->eventname}\' has been triggered in \'{$a->moduleinstance}\' {$a->frequency} time(s) in {$a->minutes} minute(s).';
$string['subscribeto'] = 'Subscribe to rule "{$a}"';
$string['taskcleanevents'] = 'Removes any unnecessary event monitor events';
+$string['taskchecksubscriptions'] = 'Activate/deactivate invalid rule subscriptions';
$string['unsubscribe'] = 'Unsubscribe';
--- /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/>.
+defined('MOODLE_INTERNAL') || exit();
+
+/**
+ * Unit tests for the subscription class.
+ * @since 3.2.0
+ *
+ * @package tool_monitor
+ * @category test
+ * @copyright 2016 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_subscription_testcase extends advanced_testcase {
+
+ /**
+ * @var \tool_monitor\subscription $subscription object.
+ */
+ private $subscription;
+
+ /**
+ * Test set up.
+ */
+ public function setUp() {
+ $this->resetAfterTest(true);
+
+ // Create the mock subscription.
+ $sub = new stdClass();
+ $sub->id = 100;
+ $sub->name = 'My test rule';
+ $sub->courseid = 20;
+ $this->subscription = $this->getMock('\tool_monitor\subscription',null, array($sub));
+ }
+
+ /**
+ * Test for the magic __isset method.
+ */
+ public function test_magic_isset() {
+ $this->assertEquals(true, isset($this->subscription->name));
+ $this->assertEquals(true, isset($this->subscription->courseid));
+ $this->assertEquals(false, isset($this->subscription->ruleid));
+ }
+
+ /**
+ * Test for the magic __get method.
+ */
+ public function test_magic_get() {
+ $this->assertEquals(20, $this->subscription->courseid);
+ $this->setExpectedException('coding_exception');
+ $this->subscription->ruleid;
+ }
+}
--- /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/>.
+defined('MOODLE_INTERNAL') || exit();
+
+/**
+ * Unit tests for the tool_monitor clean events task.
+ * @since 3.2.0
+ *
+ * @package tool_monitor
+ * @category test
+ * @copyright 2016 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_task_check_subscriptions_testcase extends advanced_testcase {
+
+ private $course;
+ private $user;
+ private $rule;
+ private $subscription;
+ private $teacherrole;
+ private $studentrole;
+
+ /**
+ * Test set up.
+ */
+ public function setUp() {
+ global $DB;
+ set_config('enablemonitor', 1, 'tool_monitor');
+ $this->resetAfterTest(true);
+
+ // All tests defined herein need a user, course, rule and subscription, so set these up.
+ $this->user = $this->getDataGenerator()->create_user();
+ $this->course = $this->getDataGenerator()->create_course();
+
+ $rule = new stdClass();
+ $rule->userid = 2; // Rule created by admin.
+ $rule->courseid = $this->course->id;
+ $rule->plugin = 'mod_book';
+ $rule->eventname = '\mod_book\event\course_module_viewed';
+ $rule->timewindow = 500;
+ $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+ $this->rule = $monitorgenerator->create_rule($rule);
+
+ $sub = new stdClass();
+ $sub->courseid = $this->course->id;
+ $sub->userid = $this->user->id;
+ $sub->ruleid = $this->rule->id;
+ $this->subscription = $monitorgenerator->create_subscription($sub);
+
+ // Also set up a student and a teacher role for use in some tests.
+ $this->teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+ $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ }
+
+ /**
+ * Reloads the subscription object from the DB.
+ *
+ * @return void.
+ */
+ private function reload_subscription() {
+ global $DB;
+ $sub = $DB->get_record('tool_monitor_subscriptions', array('id' => $this->subscription->id));
+ $this->subscription = new \tool_monitor\subscription($sub);
+ }
+
+ /**
+ * Test to confirm the task is named correctly.
+ */
+ public function test_task_name() {
+ $task = new \tool_monitor\task\check_subscriptions();
+ $this->assertEquals(get_string('taskchecksubscriptions', 'tool_monitor'), $task->get_name());
+ }
+
+ /**
+ * Test to confirm that site level subscriptions are activated and deactivated according to system capabilities.
+ */
+ public function test_site_level_subscription() {
+ // Create a site level subscription.
+ $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+ $sub = new stdClass();
+ $sub->userid = $this->user->id;
+ $sub->ruleid = $this->rule->id;
+ $this->subscription = $monitorgenerator->create_subscription($sub);
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should be inactive as the user doesn't have the capability. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Now, assign the user as a teacher role at system context.
+ $this->getDataGenerator()->role_assign($this->teacherrole->id, $this->user->id, context_system::instance());
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should be active now. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that if the module is disabled, no changes are made to active subscriptions.
+ */
+ public function test_module_disabled() {
+ set_config('enablemonitor', 0, 'tool_monitor');
+
+ // Subscription should be active to start with.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Run the task. Note, we never enrolled the user.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should still be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm an active, valid subscription stays active once the scheduled task is run.
+ */
+ public function test_active_unaffected() {
+ // Enrol the user as a teacher. This role should have the required capability.
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+ // Subscription should be active to start with.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should still be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that a subscription for a user without an enrolment to the course is made inactive.
+ */
+ public function test_course_enrolment() {
+ // Subscription should be active until deactivated by the scheduled task. Remember, by default the test setup
+ // doesn't enrol the user, so the first run of the task should deactivate it.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should NOT be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Enrol the user.
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // Subscription should now be active again.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that subscriptions for enrolled users without the required capability are made inactive.
+ */
+ public function test_enrolled_user_with_no_capability() {
+ // Enrol the user. By default, students won't have the required capability.
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->studentrole->id);
+
+ // The subscription should be active to start with. Pass in the id only to refetch the data.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should NOT be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that subscriptions for users who fail can_access_course(), are deactivated.
+ */
+ public function test_can_access_course() {
+ // Enrol the user as a teacher. This role should have the required capability.
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+ // Strip the ability to see hidden courses, so we'll fail the check_subscriptions->user_can_access_course call.
+ $context = \context_course::instance($this->course->id);
+ assign_capability('moodle/course:viewhiddencourses', CAP_PROHIBIT, $this->teacherrole->id, $context);
+
+ // Subscription should be active to start with.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Hide the course.
+ course_change_visibility($this->course->id, false);
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should be inactive. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that subscriptions for enrolled users who don't have CM access, are deactivated.
+ */
+ public function test_cm_access() {
+ // Enrol the user as a student but grant to ability to subscribe. Students cannot view hidden activities.
+ $context = \context_course::instance($this->course->id);
+ assign_capability('tool/monitor:subscribe', CAP_ALLOW, $this->studentrole->id, $context);
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->studentrole->id);
+
+ // Generate a course module.
+ $book = $this->getDataGenerator()->create_module('book', array('course' => $this->course->id));
+
+ // And add a subscription to it.
+ $sub = new stdClass();
+ $sub->courseid = $this->course->id;
+ $sub->userid = $this->user->id;
+ $sub->ruleid = $this->rule->id;
+ $sub->cmid = $book->cmid;
+ $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+ $this->subscription = $monitorgenerator->create_subscription($sub);
+
+ // The subscription should be active to start with. Pass in the id only to refetch the data.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should still be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Make the course module invisible, which should in turn make the subscription inactive.
+ set_coursemodule_visible($book->cmid, false);
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should NOT be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Make the course module visible again.
+ set_coursemodule_visible($book->cmid, true);
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should be active. Pass in the id only to refetch the data.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that long term inactive subscriptions are removed entirely.
+ */
+ public function test_stale_subscription_removal() {
+ global $DB;
+ // Manually set the inactivedate to 1 day older than the limit allowed.
+ $daysold = 1 + \tool_monitor\subscription_manager::INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS;
+
+ $inactivedate = strtotime("-$daysold days", time());
+ $DB->set_field('tool_monitor_subscriptions', 'inactivedate', $inactivedate, array('id' => $this->subscription->id));
+
+ // Subscription should be inactive to start with.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // Subscription should now not exist at all.
+ $this->assertEquals(false, $DB->record_exists('tool_monitor_subscriptions', array('id' => $this->subscription->id)));
+ }
+
+ /**
+ * Test to confirm that subscriptions for a partially set up user are deactivated.
+ */
+ public function test_user_not_fully_set_up() {
+ global $DB;
+
+ // Enrol the user as a teacher.
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+ // The subscription should be active to start.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Unset the user's email address, so we fail the check_subscriptions->is_user_setup() call.
+ $DB->set_field('user', 'email', '', array('id' => $this->user->id));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should now be inactive.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+
+ /**
+ * Test to confirm that a suspended user's subscriptions are deactivated properly.
+ */
+ public function test_suspended_user() {
+ global $DB;
+
+ // Enrol the user as a teacher. This role should have the required capability.
+ $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+ // Subscription should be active to start with.
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Suspend the user.
+ $DB->set_field('user', 'suspended', '1', array('id' => $this->user->id));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should now be inactive.
+ $this->reload_subscription();
+ $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+ // Unsuspend the user.
+ $DB->set_field('user', 'suspended', '0', array('id' => $this->user->id));
+
+ // Run the task.
+ $task = new \tool_monitor\task\check_subscriptions();
+ $task->execute();
+
+ // The subscription should now be active again.
+ $this->reload_subscription();
+ $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+ }
+}
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2016052305; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2016051900; // Requires this Moodle version.
$plugin->component = 'tool_monitor'; // Full name of the plugin (used for diagnostics).
$o.= ' <input type="hidden" name ="name" value="' . s($field->getName()) .'" />';
$o.= ' <tr valign="top"><td>Name:</td><td colspan="2">' . s($field->getName()) . '</td></tr>';
} else {
- $o.= ' <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="30" maxlength="30" id="name" value="' . s($field->getName()) . '" /></td></tr>';
+ $o.= ' <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="'.xmldb_field::NAME_MAX_LENGTH.'" maxlength="'.xmldb_field::NAME_MAX_LENGTH.'" id="name" value="' . s($field->getName()) . '" /></td></tr>';
}
// XMLDB field comment
$o.= ' <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td colspan="2"><textarea name="comment" rows="3" cols="80" id="comment">' . s($field->getComment()) . '</textarea></td></tr>';
if ($structure->getIndexUses($table->getName(), $index->getName())) {
$disabled = ' disabled="disabled " ';
}
- $o.= ' <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="30" id="name"' . $disabled . ' value="' . s($index->getName()) . '" /></td></tr>';
+ $o.= ' <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="'.xmldb_field::NAME_MAX_LENGTH.'" id="name"' . $disabled . ' value="' . s($index->getName()) . '" /></td></tr>';
// XMLDB key comment
$o.= ' <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td colspan="2"><textarea name="comment" rows="3" cols="80" id="comment">' . s($index->getComment()) . '</textarea></td></tr>';
// xmldb_index Type
if ($structure->getKeyUses($table->getName(), $key->getName())) {
$disabled = ' disabled="disabled " ';
}
- $o.= ' <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="30" id="name"' . $disabled . ' value="' . s($key->getName()) . '" /></td></tr>';
+ $o.= ' <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="'.xmldb_field::NAME_MAX_LENGTH.'" id="name"' . $disabled . ' value="' . s($key->getName()) . '" /></td></tr>';
// XMLDB key comment
$o.= ' <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td colspan="2"><textarea name="comment" rows="3" cols="80" id="comment">' . s($key->getComment()) . '</textarea></td></tr>';
// xmldb_key Type
if ($structure->getTableUses($table->getName())) {
$o.= ' <tr valign="top"><td>Name:</td><td><input type="hidden" name ="name" value="' . s($table->getName()) . '" />' . s($table->getName()) .'</td></tr>';
} else {
- $o.= ' <tr valign="top"><td><label for="name" accesskey="p">Name:</label></td><td><input name="name" type="text" size="28" maxlength="28" id="name" value="' . s($table->getName()) . '" /></td></tr>';
+ $o.= ' <tr valign="top"><td><label for="name" accesskey="p">Name:</label></td><td><input name="name" type="text" size="'.xmldb_table::NAME_MAX_LENGTH.'" maxlength="'.xmldb_table::NAME_MAX_LENGTH.'" id="name" value="' . s($table->getName()) . '" /></td></tr>';
}
$o.= ' <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td><textarea name="comment" rows="3" cols="80" id="comment">' . s($table->getComment()) . '</textarea></td></tr>';
$o.= ' <tr valign="top"><td> </td><td><input type="submit" value="' .$this->str['change'] . '" /></td></tr>';
public static function can_read($courseid) {
$context = context_course::instance($courseid);
- $capabilities = array('moodle/competency:coursecompetencyview');
+ $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
return has_any_capability($capabilities, $context);
}
if (!empty($sort)) {
$sortcolumns = explode(',', $sort);
- $sortcolumns = array_map('trim', $sortcolumns);
- $sort = ' ORDER BY e.' . implode(', e.', $sortcolumns) . ' ' . $order;
+ array_walk($sortcolumns, function(&$sortcolumn, $key, $order) {
+ $sortcolumn = trim($sortcolumn) . ' ' . $order;
+ }, $order);
+ $sort = ' ORDER BY e.' . implode(', e.', $sortcolumns);
}
$sql = 'SELECT e.*
{
+ "name": "moodle/moodle",
+ "license": "GPL-3.0",
+ "description": "Moodle - the world's open source learning platform",
+ "type": "project",
+ "homepage": "https://moodle.org",
"require-dev": {
"phpunit/phpunit": "4.8.*",
"phpunit/dbUnit": "1.4.*",
$sectionvalues['id'] = $section->id;
$sectionvalues['name'] = get_section_name($course, $section);
$sectionvalues['visible'] = $section->visible;
+
+ $options = (object) array('noclean' => true);
list($sectionvalues['summary'], $sectionvalues['summaryformat']) =
external_format_text($section->summary, $section->summaryformat,
- $context->id, 'course', 'section', $section->id);
+ $context->id, 'course', 'section', $section->id, $options);
$sectionvalues['section'] = $section->section;
$sectioncontents = array();
$courseinfo = array();
$courseinfo['id'] = $course->id;
- $courseinfo['fullname'] = $course->fullname;
- $courseinfo['shortname'] = $course->shortname;
+ $courseinfo['fullname'] = external_format_string($course->fullname, $context->id);
+ $courseinfo['shortname'] = external_format_string($course->shortname, $context->id);
$courseinfo['displayname'] = external_format_string(get_course_display_name_for_list($course), $context->id);
$courseinfo['categoryid'] = $course->category;
list($courseinfo['summary'], $courseinfo['summaryformat']) =
}
if (isset($value)) {
- $conditions[$key] = $crit['value'];
+ $conditions[$key] = $value;
$wheres[] = $key . " = :" . $key;
}
}
$this->assertEquals(1, count($categories));
+ // Same query, but forcing a parameters clean.
+ $categories = core_course_external::get_categories(array(
+ array('key' => 'id', 'value' => "$category1->id"),
+ array('key' => 'idnumber', 'value' => $category1->idnumber),
+ array('key' => 'name', 'value' => $category1->name . "<br/>"),
+ array('key' => 'visible', 'value' => '1')), 0);
+ $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
+ $this->assertEquals(1, count($categories));
+
// Retrieve categories from parent.
$categories = core_course_external::get_categories(array(
array('key' => 'parent', 'value' => $category3->id)), 1);
$generatedcourses = array();
$coursedata['idnumber'] = 'idnumbercourse1';
- $coursedata['fullname'] = 'Course 1 for PHPunit test';
+ // Adding tags here to check that format_string is applied.
+ $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
+ $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
$coursedata['summary'] = 'Course 1 description';
$coursedata['summaryformat'] = FORMAT_MOODLE;
$course1 = self::getDataGenerator()->create_course($coursedata);
$this->assertEquals(2, count($courses));
foreach ($courses as $course) {
+ $coursecontext = context_course::instance($course['id']);
$dbcourse = $generatedcourses[$course['id']];
$this->assertEquals($course['idnumber'], $dbcourse->idnumber);
- $this->assertEquals($course['fullname'], $dbcourse->fullname);
- $this->assertEquals($course['displayname'], get_course_display_name_for_list($dbcourse));
+ $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id));
+ $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse),
+ $coursecontext->id));
// Summary was converted to the HTML format.
$this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
$this->assertEquals($course['summaryformat'], FORMAT_HTML);
- $this->assertEquals($course['shortname'], $dbcourse->shortname);
+ $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id));
$this->assertEquals($course['categoryid'], $dbcourse->category);
$this->assertEquals($course['format'], $dbcourse->format);
$this->assertEquals($course['showgrades'], $dbcourse->showgrades);
* @return array A list with the course object and course modules objects
*/
private function prepare_get_course_contents_test() {
+ global $DB;
$course = self::getDataGenerator()->create_course();
$forumdescription = 'This is the forum description';
$forum = $this->getDataGenerator()->create_module('forum',
$roleid = $this->assignUserCapability('moodle/course:view', $context->id);
$this->assignUserCapability('moodle/course:update', $context->id, $roleid);
+ $conditions = array('course' => $course->id, 'section' => 2);
+ $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
+ rebuild_course_cache($course->id, true);
+
return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
}
$this->assertCount(4, $firstsection['modules']);
$this->assertCount(1, $lastsection['modules']);
$this->assertEquals(2, $lastsection['section']);
+ $this->assertContains('<iframe', $lastsection['summary']);
+ $this->assertContains('</iframe>', $lastsection['summary']);
try {
$sections = core_course_external::get_course_contents($course->id,
$params = self::validate_parameters(self::get_users_courses_parameters(), array('userid'=>$userid));
$courses = enrol_get_users_courses($params['userid'], true, 'id, shortname, fullname, idnumber, visible,
- summary, summaryformat, format, showgrades, lang, enablecompletion');
+ summary, summaryformat, format, showgrades, lang, enablecompletion, category');
$result = array();
foreach ($courses as $course) {
list($course->summary, $course->summaryformat) =
external_format_text($course->summary, $course->summaryformat, $context->id, 'course', 'summary', null);
-
- $result[] = array('id' => $course->id, 'shortname' => $course->shortname, 'fullname' => $course->fullname,
- 'idnumber' => $course->idnumber, 'visible' => $course->visible, 'enrolledusercount' => $enrolledusercount,
- 'summary' => $course->summary, 'summaryformat' => $course->summaryformat, 'format' => $course->format,
- 'showgrades' => $course->showgrades, 'lang' => $course->lang, 'enablecompletion' => $course->enablecompletion
- );
+ $course->fullname = external_format_string($course->fullname, $context->id);
+ $course->shortname = external_format_string($course->shortname, $context->id);
+
+ $result[] = array(
+ 'id' => $course->id,
+ 'shortname' => $course->shortname,
+ 'fullname' => $course->fullname,
+ 'idnumber' => $course->idnumber,
+ 'visible' => $course->visible,
+ 'enrolledusercount' => $enrolledusercount,
+ 'summary' => $course->summary,
+ 'summaryformat' => $course->summaryformat,
+ 'format' => $course->format,
+ 'showgrades' => $course->showgrades,
+ 'lang' => $course->lang,
+ 'enablecompletion' => $course->enablecompletion,
+ 'category' => $course->category
+ );
}
return $result;
'showgrades' => new external_value(PARAM_BOOL, 'true if grades are shown, otherwise false', VALUE_OPTIONAL),
'lang' => new external_value(PARAM_LANG, 'forced course language', VALUE_OPTIONAL),
'enablecompletion' => new external_value(PARAM_BOOL, 'true if completion is enabled, otherwise false',
- VALUE_OPTIONAL)
+ VALUE_OPTIONAL),
+ 'category' => new external_value(PARAM_INT, 'course category id', VALUE_OPTIONAL),
)
)
);
$this->resetAfterTest(true);
$coursedata1 = array(
+ 'fullname' => '<b>Course 1</b>', // Adding tags here to check that external_format_string works.
+ 'shortname' => '<b>Course 1</b>', // Adding tags here to check that external_format_string works.
'summary' => 'Lightwork Course 1 description',
'summaryformat' => FORMAT_MOODLE,
'lang' => 'en',
// Check there are no differences between $course1 properties and course values returned by the webservice
// only for those fields listed in the $coursedata1 array.
+ $course1->fullname = external_format_string($course1->fullname, $contexts[$course1->id]->id);
+ $course1->shortname = external_format_string($course1->shortname, $contexts[$course1->id]->id);
foreach ($enrolledincourses as $courseenrol) {
if ($courseenrol['id'] == $course1->id) {
foreach ($coursedata1 as $fieldname => $value) {
This files describes API changes in /enrol/* - plugins,
information provided here is intended especially for developers.
+=== 3.2 ===
+
+* External function core_enrol_external::get_users_courses now return the category id as an additional optional field.
+
=== 3.1 ===
* core_enrol_external::get_enrolled_users now supports two additional parameters for ordering: sortby and sortdirection.
--- /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/>.
+
+/**
+ * Enhance the gradebook tree setup with various facilities.
+ *
+ * @module core_grades/edittree_index
+ * @package core_grades
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+ 'jquery',
+], function($) {
+ /**
+ * Enhance the edittree functionality.
+ *
+ * @method edittree
+ */
+ var edittree = function() {
+ // Watch items and toggle the move menu accordingly.
+ $('body').on('change', '.itemselect.ignoredirty', edittree.checkMoveMenuState);
+
+ // Watch for the 'All' and 'None' links.
+ $('body').on('click', '[data-action="grade_edittree-index-bulkselect"]', edittree.toggleAllSelectItems);
+
+ // Watch for the weight override checkboxes.
+ $('body').on('change', '.weightoverride', edittree.toggleWeightInput);
+
+ // Watch changes to the bulk move menu and submit.
+ $('#menumoveafter').on('change', function() {
+ var form = $(this).closest('form'),
+ bulkmove = form.find('#bulkmoveinput');
+
+ bulkmove.val(1);
+ form.submit();
+ });
+
+ // CHeck the initial state of the move menu.
+ edittree.checkMoveMenuState();
+ };
+
+ /**
+ * Toggle the weight input field based on its checkbox.
+ *
+ * @method toggleWeightInput
+ * @param {EventFacade} e
+ * @private
+ */
+ edittree.toggleWeightInput = function(e) {
+ e.preventDefault();
+ var node = $(this),
+ row = node.closest('tr');
+
+ $('input[name="weight_' + row.data('itemid') + '"]').prop('disabled', !node.prop('checked'));
+ };
+
+ /**
+ * Toggle all select boxes on or off.
+ *
+ * @method toggleAllSelectItems
+ * @param {EventFacade} e
+ * @private
+ */
+ edittree.toggleAllSelectItems = function(e) {
+ e.preventDefault();
+
+ var node = $(this),
+ row = node.closest('tr');
+ $('.' + row.data('category') + ' .itemselect').prop('checked', node.data('checked'));
+
+ edittree.checkMoveMenuState();
+ };
+
+ /**
+ * Get the move menu.
+ *
+ * @method getMoveMenu
+ * @private
+ * @return {jQuery}
+ */
+ edittree.getMoveMenu = function() {
+ return $('#menumoveafter');
+ };
+
+ /**
+ * Check whether any checkboxes are ticked.
+ *
+ * @method checkMoveMenuState
+ * @private
+ * @return {Boolean}
+ */
+ edittree.checkMoveMenuState = function() {
+ var menu = edittree.getMoveMenu();
+ if (!menu.length) {
+ return false;
+ }
+
+ var selected;
+ $('.itemselect').each(function() {
+ selected = $(this).prop('checked');
+
+ // Return early if any are checked.
+ return !selected;
+ });
+
+ menu.prop('disabled', !selected);
+
+ return selected;
+ };
+
+ return /** @alias module:core_grades/edittree_index */ {
+ enhance: edittree
+ };
+});
+++ /dev/null
-/**
- * Toggles the selection checkboxes of all grade items children of the given eid (a category id)
- */
-function togglecheckboxes(event, args) {
-YUI().use('yui2-dom', 'yui2-element', function (Y) {
-
- var rows = Y.YUI2.util.Dom.getElementsByClassName(args.eid);
-
- for (var i = 0; i < rows.length; i++) {
- var element = new Y.YUI2.util.Element(rows[i]);
- var checkboxes = element.getElementsByClassName('itemselect');
- if (checkboxes[0]) {
- checkboxes[0].checked=args.check;
- }
- }
-
- toggleCategorySelector();
-
-});
-}
-
-function toggle_advanced_columns() {
-YUI().use('yui2-dom', function (Y) {
-
- var advEls = Y.YUI2.util.Dom.getElementsByClassName("advanced");
- var shownAdvEls = Y.YUI2.util.Dom.getElementsByClassName("advancedshown");
-
- for (var i = 0; i < advEls.length; i++) {
- Y.YUI2.util.Dom.replaceClass(advEls[i], "advanced", "advancedshown");
- }
-
- for (var i = 0; i < shownAdvEls.length; i++) {
- Y.YUI2.util.Dom.replaceClass(shownAdvEls[i], "advancedshown", "advanced");
- }
-
-});
-}
-
-/**
- * Check if any of the grade item checkboxes is ticked. If yes, enable the dropdown. Otherwise, disable it
- */
-function toggleCategorySelector() {
-YUI().use('yui2-dom', function (Y) {
-
- var menumoveafter = document.getElementById('menumoveafter');
- if (!menumoveafter) {
- return;
- }
-
- var itemboxes = Y.YUI2.util.Dom.getElementsByClassName('itemselect');
- for (var i = 0; i < itemboxes.length; i++) {
- if (itemboxes[i].checked) {
- menumoveafter.disabled = false;
- return true;
- }
- }
- menumoveafter.disabled = 'disabled';
-
-});
-}
-
-function submit_bulk_move(e, args) {
- document.getElementById('bulkmoveinput').value = 1;
- document.getElementById('gradetreeform').submit();
-}
-
-function update_category_aggregation(e, args) {
- var selectmenu = e.target;
- window.location = 'index.php?id='+args.courseid+'&category='+args.category+'&aggregationtype='+selectmenu.get('value')+'&sesskey='+args.sesskey;
-}
-
-/**
- * The weight override checkboxes toggle the disabled status of their associated weight fields.
- */
-YUI().use('node', 'delegate', function(Y) {
- Y.on('domready', function() {
- Y.delegate('click', function(e) {
- var t = e.currentTarget,
- itemid = t.get('id').split('_')[1];
- Y.one('input[name=weight_' + itemid + ']').set('disabled', t.get('checked') ? false : true);
- }, Y.config.doc.body, 'input.weightoverride');
- });
-});
-
-/* TODO: finish and rewrite for YUI3...
-Y.YUI2.namespace('grade_edit_tree');
-
-(function() {
- var Dom = Y.YUI2.util.Dom;
- var DDM = Y.YUI2.util.DragDropMgr;
- var Event = Y.YUI2.util.Event;
- var gretree = Y.YUI2.grade_edit_tree;
-
- gretree.DDApp = {
-
- init: function() {
-
- var edit_tree_table = Dom.get('grade_edit_tree_table');
- var i;
- var item_rows = edit_tree_table.getElementsByClassName('item', 'tr');
- var category_rows = edit_tree_table.getElementsByClassName('category', 'tr');
-
- new Y.YUI2.util.DDTarget('grade_edit_tree_table');
-
- for (i = 0; i < item_rows.length; i++) {
- if (!Dom.hasClass(item_rows[i],'categoryitem')) {
- new gretree.DDList(item_rows[i]);
- }
- }
-
- for (i = 0; i < category_rows.length; i++) {
- if (!Dom.hasClass(category_rows[i],'coursecategory')) {
- // Find the cell that spans rows for this category
- var rowspancell = category_rows[i].getElementsByClassName('name', 'td');
- var rowspan = parseInt(rowspancell[0].previousSibling.rowSpan) + 1;
- var rows = Array(rowspan);
- var lastRow = category_rows[i];
-
- for (var j = 0; j < rowspan; j++) {
- rows[j] = lastRow;
- lastRow = lastRow.nextSibling;
- }
-
- new gretree.DDList(rows);
- }
- }
-
- Y.YUI2.util.Event.on("showButton", "click", this.showOrder);
- Y.YUI2.util.Event.on("switchButton", "click", this.switchStyles);
- },
-
- showOrder: function() {
- var parseTable = function(table, title) {
- var items = table.getElementsByTagName('tr');
- var out = title + ": ";
-
- for (i = 0; i < items.length; i++) {
- out += items[i].id + ' ';
- }
- return out;
- };
-
- var table = Dom.get('grade_edit_tree_table');
- alert(parseTable(table, "Grade edit tree table"));
- },
-
- switchStyles: function() {
- Dom.get('grade_edit_tree_table').className = 'draglist_alt';
- }
- };
-
- gretree.DDList = function(id, sGroup, config) {
-
- gretree.DDList.superclass.constructor.call(this, id, sGroup, config);
- this.logger = this.logger || Y.YUI2;
- var el = this.getDragEl();
- Dom.setStyle(el, 'opacity', 0.67);
-
- this.goingUp = false;
- this.lastY = 0;
- };
-
- Y.YUI2.extend(gretree.DDList, Y.YUI2.util.DDProxy, {
-
- startDrag: function(x, y) {
- this.logger.log(this.id + ' startDrag');
-
- // Make the proxy look like the source element
- var dragEl = this.getDragEl();
- var clickEl = this.getEl();
-
- Dom.setStyle(clickEl, 'visibility', 'hidden');
-
- dragEl.innerHTML = clickEl.innerHTML;
-
- Dom.setStyle(dragEl, 'color', Dom.getStyle(clickEl, 'color'));
- Dom.setStyle(dragEl, 'backgroundColor', Dom.getStyle(clickEl, 'backgroundColor'));
- Dom.setStyle(dragEl, 'border', '2px solid gray');
- },
-
- endDrag: function(e) {
- this.logger.log(this.id + ' endDrag');
- var srcEl = this.getEl();
- var proxy = this.getDragEl();
-
- // Show the proxy element and adnimate it to the src element's location
- Dom.setStyle(proxy, 'visibility', '');
- var a = new Y.YUI2.util.Motion(proxy, { points: { to: Dom.getXY(srcEl) } }, 0.2, Y.YUI2.util.Easing.easeOut);
- var proxyid = proxy.id;
- var thisid = this.id;
-
- // Hide the proxy and show the source element when finished with the animation
- a.onComplete.subscribe(function() {
- Dom.setStyle(proxyid, 'visibility', 'hidden');
- Dom.setStyle(thisid, 'visibility', '');
- });
-
- a.animate();
- },
-
- onDragDrop: function(e, id) {
- this.logger.log(this.id + ' dragDrop');
-
- // If there is one drop interaction, the tr was dropped either on the table, or it was dropped on the current location of the source element
-
- if (DDM.interactionInfo.drop.length === 1) {
- // The position of the cursor at the time of the drop (Y.YUI2.util.Point)
- var pt = DDM.interactionInfo.point;
-
- // The region occupied by the source element at the time of the drop
- var region = DDM.interactionInfo.sourceRegion;
-
- // Check to see if we are over the source element's location. We will append to the bottom of the list once we are sure it was a drop in the negative space
- if (!region.intersect(pt)) {
- var destEl = Dom.get(id);
- var destDD = DDM.getDDById(id);
- destEl.appendChild(this.getEl());
- destDD.isEmpty = false;
- DDM.refreshCache();
- }
- }
- },
-
- onDrag: function(e) {
-
- // Keep track of the direction of the drag for use during onDragOver
- var y = Event.getPageY(e);
-
- if (y < this.lastY) {
- this.goingUp = true;
- } else if (y > this.lastY) {
- this.goingUp = false;
- }
-
- this.lastY = y;
- },
-
- onDragOver: function(e, id) {
- var srcEl = this.getEl();
- var destEl = Dom.get(id);
-
- // We are only concerned with tr items, we ignore the dragover notifications for the table
- if (destEl.nodeName.toLowerCase() == 'tr') {
- var orig_p = srcEl.parentNode;
- var p = destEl.parentNode;
-
- if (this.goingup) {
- p.insertBefore(srcEl, destEl); // insert above
- } else {
- p.insertBefore(srcEl, destEl.nextSibling); // insert below
- }
-
- DDM.refreshCache();
- }
- }
- });
- // Y.YUI2.util.Event.onDOMReady(gretree.DDApp.init, gretree.DDApp, true); // Uncomment this line when dragdrop is fully implemented
-})();
-*/
\ No newline at end of file
$context = context_course::instance($course->id);
require_capability('moodle/grade:manage', $context);
-// todo $PAGE->requires->js_module() should be used here instead
-$PAGE->requires->js('/grade/edit/tree/functions.js');
+$PAGE->requires->js_call_amd('grades/edittree_index', 'enhance');
/// return tracking object
$gpr = new grade_plugin_return(array('type'=>'edit', 'plugin'=>'tree', 'courseid'=>$courseid));
$attributes = array('id'=>'menumoveafter', 'class' => 'ignoredirty singleselect');
echo html_writer::label(get_string('moveselectedto', 'grades'), 'menumoveafter');
echo html_writer::select($grade_edit_tree->categories, 'moveafter', '', array(''=>'choosedots'), $attributes);
- $OUTPUT->add_action_handler(new component_action('change', 'submit_bulk_move'), 'menumoveafter');
echo '<div id="noscriptgradetreeform" class="hiddenifjs">
<input type="submit" value="'.get_string('go').'" />
</div>';
$row = new html_table_row();
$row->id = 'grade-item-' . $eid;
$row->attributes['class'] = $courseclass . ' category ' . $dimmed;
+ $row->attributes['data-category'] = $eid;
+ $row->attributes['data-itemid'] = $category->get_grade_item()->id;
foreach ($rowclasses as $class) {
$row->attributes['class'] .= ' ' . $class;
}
foreach ($this->columns as $column) {
if (!($this->moving && $column->hide_when_moving)) {
- $row->cells[] = $column->get_category_cell($category, $levelclass, array('id' => $id,
- 'name' => $object->name, 'level' => $level, 'actions' => $actions,
- 'moveaction' => $moveaction, 'eid' => $eid));
+ $row->cells[] = $column->get_category_cell($category, $levelclass, [
+ 'id' => $id,
+ 'name' => $object->name,
+ 'level' => $level,
+ 'actions' => $actions,
+ 'moveaction' => $moveaction,
+ 'eid' => $eid,
+ ]);
}
}
$gradeitemrow = new html_table_row();
$gradeitemrow->id = 'grade-item-' . $eid;
$gradeitemrow->attributes['class'] = $categoryitemclass . ' item ' . $dimmed;
+ $gradeitemrow->attributes['data-itemid'] = $object->id;
foreach ($rowclasses as $class) {
$gradeitemrow->attributes['class'] .= ' ' . $class;
}
$str = '';
if ($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoef' || $aggcoef == 'aggregationcoefextraweight') {
- return '<label class="accesshide" for="weight_'.$item->id.'">'.
- get_string('extracreditvalue', 'grades', $itemname).'</label>'.
- '<input type="text" size="6" id="weight_'.$item->id.'" name="weight_'.$item->id.'"
- value="'.grade_edit_tree::format_number($item->aggregationcoef).'" />';
+ return '<label class="accesshide" for="weight_'.$item->id.'">' .
+ get_string('extracreditvalue', 'grades', $itemname).'</label>' .
+ html_writer::empty_tag('input', [
+ 'type' => 'text',
+ 'size' => 6,
+ 'id' => 'weight_' . $item->id,
+ 'name' => 'weight_' . $item->id,
+ 'value' => self::format_number($item->aggregationcoef),
+ ]);
+
} else if ($aggcoef == 'aggregationcoefextraweightsum') {
$checkboxname = 'weightoverride_' . $item->id;
$checkboxlbl = html_writer::tag('label', get_string('overrideweightofa', 'grades', $itemname),
array('for' => $checkboxname, 'class' => 'accesshide'));
- $checkbox = html_writer::empty_tag('input', array('name' => $checkboxname,
- 'type' => 'hidden', 'value' => 0));
- $checkbox .= html_writer::empty_tag('input', array('name' => $checkboxname,
- 'type' => 'checkbox', 'value' => 1, 'id' => $checkboxname, 'class' => 'weightoverride',
- 'checked' => ($item->weightoverride ? 'checked' : null)));
+ $checkbox = html_writer::empty_tag('input', [
+ 'name' => $checkboxname,
+ 'type' => 'hidden',
+ 'value' => 0,
+ ]);
+
+ $checkbox .= html_writer::empty_tag('input', [
+ 'name' => $checkboxname,
+ 'type' => 'checkbox',
+ 'value' => 1,
+ 'id' => $checkboxname,
+ 'class' => 'weightoverride',
+ 'checked' => ($item->weightoverride ? 'checked' : null),
+ ]);
$name = 'weight_' . $item->id;
$hiddenlabel = html_writer::tag(
}
public function get_category_cell($category, $levelclass, $params) {
- global $OUTPUT;
if (empty($params['eid'])) {
throw new Exception('Array key (eid) missing from 3rd param of grade_edit_tree_column_select::get_category_cell($category, $levelclass, $params)');
}
- $selectall = new action_link(new moodle_url('#'), get_string('all'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => true)));
- $selectnone = new action_link(new moodle_url('#'), get_string('none'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => false)));
+ $selectall = html_writer::link('#', get_string('all'), [
+ 'data-action' => 'grade_edittree-index-bulkselect',
+ 'data-checked' => true,
+ ]);
+ $selectnone = html_writer::link('#', get_string('none'), [
+ 'data-action' => 'grade_edittree-index-bulkselect',
+ 'data-checked' => false,
+ ]);
$categorycell = parent::get_category_cell($category, $levelclass, $params);
- $categorycell->text = $OUTPUT->render($selectall) . ' / ' . $OUTPUT->render($selectnone);
+ $categorycell->text = $selectall . ' / ' . $selectnone;
return $categorycell;
}
$itemcell->text = '<label class="accesshide" for="select_'.$params['eid'].'">'.
get_string('select', 'grades', $item->itemname).'</label>
<input class="itemselect ignoredirty" type="checkbox" name="select_'.$params['eid'].'" id="select_'.$params['eid'].
- '" onchange="toggleCategorySelector();"/>'; // TODO: convert to YUI handler
+ '"/>';
}
return $itemcell;
}
@ini_set('display_errors', '1');
// Check that PHP is of a sufficient version.
-if (version_compare(phpversion(), '5.4.4') < 0) {
+if (version_compare(phpversion(), '5.6.5') < 0) {
$phpversion = phpversion();
// do NOT localise - lang strings would not work here and we CAN not move it after installib
- echo "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).<br />";
+ echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).<br />";
echo "Please upgrade your server software or install older Moodle version.";
die;
}
$string['score'] = 'Score';
$string['search'] = 'Search';
$string['search:mycourse'] = 'My courses';
+$string['search:user'] = 'Users';
$string['searcharea'] = 'Search area';
$string['searching'] = 'Searching in ...';
$string['searchnotpermitted'] = 'You are not allowed to do a search';
sesskey: config.sesskey
};
- $.post(adminurl + 'roles/ajax.php', params)
+ // Need to tell jQuery to expect JSON as the content type may not be correct (MDL-55041).
+ $.post(adminurl + 'roles/ajax.php', params, null, 'json')
.done(function(data) {
try {
overideableroles = data;
action: action,
capability: row.data('name')
};
- $.post(adminurl + 'roles/ajax.php', params)
+ $.post(adminurl + 'roles/ajax.php', params, null, 'json')
.done(function(data) {
var action = data;
try {
$badges = $backpack->get_badges($collection->collectionid);
if (isset($badges->badges)) {
$out->badges = array_merge($out->badges, $badges->badges);
- $out->totalbadges += count($out->badges);
+ $out->totalbadges += count($badges->badges);
} else {
$out->badges = array_merge($out->badges, array());
}
use core_component;
use coding_exception;
+use moodle_exception;
use SplFileInfo;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
*/
public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
+ // Extract the package into a temporary location.
$fp = get_file_packer('application/zip');
- $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
+ $tempdir = make_request_directory();
+ $files = $fp->extract_to_pathname($zipfilepath, $tempdir);
if (!$files) {
return array();
}
+ // If requested, rename the root directory of the plugin.
if (!empty($rootdir)) {
- $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
+ $files = $this->rename_extracted_rootdir($tempdir, $rootdir, $files);
}
// Sometimes zip may not contain all parent directories, add them to make it consistent.
}
}
+ // Move the extracted files into the target location.
+ $this->move_extracted_plugin_files($tempdir, $targetdir, $files);
+
// Set the permissions of extracted subdirs and files.
$this->set_plugin_files_permissions($targetdir, $files);
/**
* Renames the root directory of the extracted ZIP package.
*
- * This method does not validate the presence of the single root directory
- * (it is the validator's duty). It just searches for the first directory
- * under the given location and renames it.
- *
- * The method will not rename the root if the requested location already
- * exists.
+ * This internal helper method assumes that the plugin ZIP package has been
+ * extracted into a temporary empty directory so the plugin folder is the
+ * only folder there. The ZIP package is supposed to be validated so that
+ * it contains just a single root folder.
*
* @param string $dirname fullpath location of the extracted ZIP package
* @param string $rootdir the requested name of the root directory
continue;
}
if (is_dir($dirname.'/'.$item)) {
+ if ($found !== null and $found !== $item) {
+ // Multiple directories found.
+ throw new moodle_exception('unexpected_archive_structure', 'core_plugin');
+ }
$found = $item;
- break;
}
}
}
}
}
+
+ /**
+ * Moves the extracted contents of the plugin ZIP into the target location.
+ *
+ * @param string $sourcedir full path to the directory the ZIP file was extracted to
+ * @param mixed $targetdir full path to the directory where the files should be moved to
+ * @param array $files list of extracted files
+ */
+ protected function move_extracted_plugin_files($sourcedir, $targetdir, array $files) {
+ global $CFG;
+
+ foreach ($files as $file => $status) {
+ if ($status !== true) {
+ throw new moodle_exception('corrupted_archive_structure', 'core_plugin', '', $file, $status);
+ }
+
+ $source = $sourcedir.'/'.$file;
+ $target = $targetdir.'/'.$file;
+
+ if (is_dir($source)) {
+ continue;
+
+ } else {
+ if (!is_dir(dirname($target))) {
+ mkdir(dirname($target), $CFG->directorypermissions, true);
+ }
+ rename($source, $target);
+ }
+ }
+ }
}
// If noreply user is set then use it, else create one.
if (!empty($CFG->noreplyuserid)) {
self::$noreplyuser = self::get_user($CFG->noreplyuserid);
+ self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
+ return self::$noreplyuser;
+ } else {
+ // Do not cache the dummy user record to avoid language internationalization issues.
+ $noreplyuser = self::get_dummy_user_record();
+ $noreplyuser->maildisplay = '1'; // Show to all.
+ $noreplyuser->emailstop = 1;
+ return $noreplyuser;
}
-
- if (empty(self::$noreplyuser)) {
- self::$noreplyuser = self::get_dummy_user_record();
- self::$noreplyuser->maildisplay = '1'; // Show to all.
- }
- self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
- return self::$noreplyuser;
}
/**
// If custom support user is set then use it, else if supportemail is set then use it, else use noreply.
if (!empty($CFG->supportuserid)) {
self::$supportuser = self::get_user($CFG->supportuserid, '*', MUST_EXIST);
- }
-
- // Try sending it to support email if support user is not set.
- if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
- self::$supportuser = self::get_dummy_user_record();
- self::$supportuser->id = self::SUPPORT_USER;
- self::$supportuser->email = $CFG->supportemail;
+ } else if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
+ // Try sending it to support email if support user is not set.
+ $supportuser = self::get_dummy_user_record();
+ $supportuser->id = self::SUPPORT_USER;
+ $supportuser->email = $CFG->supportemail;
if ($CFG->supportname) {
- self::$supportuser->firstname = $CFG->supportname;
+ $supportuser->firstname = $CFG->supportname;
}
- self::$supportuser->username = 'support';
- self::$supportuser->maildisplay = '1'; // Show to all.
+ $supportuser->username = 'support';
+ $supportuser->maildisplay = '1'; // Show to all.
+ // Unset emailstop to make sure support message is sent.
+ $supportuser->emailstop = 0;
+ return $supportuser;
}
// Send support msg to admin user if nothing is set above.
'classpath' => 'course/externallib.php',
'description' => 'Return category details',
'type' => 'read',
- 'capabilities' => 'moodle/category:viewhiddencategories'
+ 'capabilities' => 'moodle/category:viewhiddencategories',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_course_get_contents' => array(
'classname' => 'core_course_external',
'methodname' => 'update_grades',
'description' => 'Update a grade item and associated student grades.',
'type' => 'write',
- 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_grading_get_definitions' => array(
'classname' => 'core_grading_external',
* Test behaviour of create_table()
*/
public function test_create_table() {
+
$DB = $this->tdb; // Do not use global $DB!
$dbman = $this->tdb->get_manager();
$this->assertInstanceOf('ddl_exception', $e);
}
- // Long table name names - the largest allowed.
- $table = new xmldb_table('test_table0123456789_____xyz');
+ // Long table name names - the largest allowed by the configuration which exclude the prefix to ensure it's created.
+ $tablechars = str_repeat('a', xmldb_table::NAME_MAX_LENGTH);
+ $table = new xmldb_table($tablechars);
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$this->assertTrue($dbman->table_exists($table));
$dbman->drop_table($table);
- // Table name is too long.
- $table = new xmldb_table('test_table0123456789_____xyz9');
+ // Table name is too long, ignoring any prefix size set.
+ $tablechars = str_repeat('a', xmldb_table::NAME_MAX_LENGTH + 1);
+ $table = new xmldb_table($tablechars);
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
// Weird column names - the largest allowed.
$table = new xmldb_table('test_table3');
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
- $table->add_field('abcdef____0123456789_______xyz', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
+ $table->add_field(str_repeat('b', xmldb_field::NAME_MAX_LENGTH), XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->setComment("This is a test'n drop table. You can drop it safely");
$this->assertTrue($dbman->table_exists($table));
$dbman->drop_table($table);
- // Too long field name - max 30.
+ // Too long field name.
$table = new xmldb_table('test_table4');
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
- $table->add_field('abcdeabcdeabcdeabcdeabcdeabcdez', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
+ $table->add_field(str_repeat('a', xmldb_field::NAME_MAX_LENGTH + 1), XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->setComment("This is a test'n drop table. You can drop it safely");
/** @var resource $pgsql database resource */
protected $pgsql = null;
- protected $bytea_oid = null;
protected $last_error_reporting; // To handle pgsql driver default verbosity
$connection = "host='$this->dbhost' $port user='$this->dbuser' password='$pass' dbname='$this->dbname'";
}
+ // ALTER USER and ALTER DATABASE are overridden by these settings.
+ $options = array('--client_encoding=utf8', '--standard_conforming_strings=on');
+ // Select schema if specified, otherwise the first one wins.
+ if (!empty($this->dboptions['dbschema'])) {
+ $options[] = "-c search_path=" . addcslashes($this->dboptions['dbschema'], "'\\");
+ }
+
+ $connection .= " options='".implode(' ', $options)."'";
+
ob_start();
if (empty($this->dboptions['dbpersist'])) {
$this->pgsql = pg_connect($connection, PGSQL_CONNECT_FORCE_NEW);
throw new dml_connection_exception($dberr);
}
- $this->query_start("--pg_set_client_encoding()", null, SQL_QUERY_AUX);
- pg_set_client_encoding($this->pgsql, 'utf8');
- $this->query_end(true);
-
- $sql = '';
- // Only for 9.0 and upwards, set bytea encoding to old format.
- if ($this->is_min_version('9.0')) {
- $sql = "SET bytea_output = 'escape'; ";
- }
-
- // Select schema if specified, otherwise the first one wins.
- if (!empty($this->dboptions['dbschema'])) {
- $sql .= "SET search_path = '".$this->dboptions['dbschema']."'; ";
- }
-
- // Find out the bytea oid.
- $sql .= "SELECT oid FROM pg_type WHERE typname = 'bytea'";
- $this->query_start($sql, null, SQL_QUERY_AUX);
- $result = pg_query($this->pgsql, $sql);
- $this->query_end($result);
-
- $this->bytea_oid = pg_fetch_result($result, 0, 0);
- pg_free_result($result);
- if ($this->bytea_oid === false) {
- $this->pgsql = null;
- throw new dml_connection_exception('Can not read bytea type.');
- }
-
// Connection stabilised and configured, going to instantiate the temptables controller
$this->temptables = new pgsql_native_moodle_temptables($this);
return array('description'=>$info['server'], 'version'=>$info['server']);
}
- /**
- * Returns if the RDBMS server fulfills the required version
- *
- * @param string $version version to check against
- * @return bool returns if the version is fulfilled (true) or no (false)
- */
- private function is_min_version($version) {
- $server = $this->get_server_info();
- $server = $server['version'];
- return version_compare($server, $version, '>=');
- }
-
/**
* Returns supported query parameter types
* @return int bitmask of accepted SQL_PARAMS_*
if (is_bool($value)) { // Always, convert boolean to int
$value = (int)$value;
- } else if ($column->meta_type === 'B') { // BLOB detected, we return 'blob' array instead of raw value to allow
- if (!is_null($value)) { // binding/executing code later to know about its nature
- $value = array('blob' => $value);
+ } else if ($column->meta_type === 'B') {
+ if (!is_null($value)) {
+ // standard_conforming_strings must be enabled, otherwise pg_escape_bytea() will double escape
+ // \ and produce data errors. This is set on the connection.
+ $value = pg_escape_bytea($this->pgsql, $value);
}
} else if ($value === '') {
}
protected function create_recordset($result) {
- return new pgsql_native_moodle_recordset($result, $this->bytea_oid);
+ return new pgsql_native_moodle_recordset($result);
}
/**
$this->query_end($result);
// find out if there are any blobs
- $numrows = pg_num_fields($result);
+ $numfields = pg_num_fields($result);
$blobs = array();
- for($i=0; $i<$numrows; $i++) {
- $type_oid = pg_field_type_oid($result, $i);
- if ($type_oid == $this->bytea_oid) {
+ for ($i = 0; $i < $numfields; $i++) {
+ $type = pg_field_type($result, $i);
+ if ($type == 'bytea') {
$blobs[] = pg_field_name($result, $i);
}
}
$id = reset($row);
if ($blobs) {
foreach ($blobs as $blob) {
- // note: in PostgreSQL 9.0 the returned blobs are hexencoded by default - see http://www.postgresql.org/docs/9.0/static/runtime-config-client.html#GUC-BYTEA-OUTPUT
- $row[$blob] = $row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null;
+ $row[$blob] = ($row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null);
}
}
if (isset($return[$id])) {
$this->query_end($result);
$return = pg_fetch_all_columns($result, 0);
+
+ if (pg_field_type($result, 0) == 'bytea') {
+ foreach ($return as $key => $value) {
+ $return[$key] = ($value === null ? $value : pg_unescape_bytea($value));
+ }
+ }
+
pg_free_result($result);
return $return;
}
$cleaned = array();
- $blobs = array();
foreach ($dataobject as $field=>$value) {
if ($field === 'id') {
continue;
}
$column = $columns[$field];
- $normalised_value = $this->normalise_value($column, $value);
- if (is_array($normalised_value) && array_key_exists('blob', $normalised_value)) {
- $cleaned[$field] = '@#BLOB#@';
- $blobs[$field] = $normalised_value['blob'];
- } else {
- $cleaned[$field] = $normalised_value;
- }
- }
-
- if (empty($blobs)) {
- return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
+ $cleaned[$field] = $this->normalise_value($column, $value);
}
- $id = $this->insert_record_raw($table, $cleaned, true, $bulk);
-
- foreach ($blobs as $key=>$value) {
- $value = pg_escape_bytea($this->pgsql, $value);
- $sql = "UPDATE {$this->prefix}$table SET $key = '$value'::bytea WHERE id = $id";
- $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
- $result = pg_query($this->pgsql, $sql);
- $this->query_end($result);
- if ($result !== false) {
- pg_free_result($result);
- }
- }
-
- return ($returnid ? $id : true);
+ return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
}
$columns = $this->get_columns($table, true);
- // Make sure there are no nasty blobs!
- foreach ($columns as $column) {
- if ($column->binary) {
- parent::insert_records($table, $dataobjects);
- return;
- }
- }
-
$fields = null;
$count = 0;
$chunk = array();
}
/**
- * Insert records in chunks, no binary support, strict param types...
+ * Insert records in chunks, strict param types...
*
* Note: can be used only from insert_records().
*
$columns = $this->get_columns($table);
$cleaned = array();
- $blobs = array();
foreach ($dataobject as $field=>$value) {
$this->detect_objects($value);
if (!isset($columns[$field])) {
continue;
}
- if ($columns[$field]->meta_type === 'B') {
- if (!is_null($value)) {
- $cleaned[$field] = '@#BLOB#@';
- $blobs[$field] = $value;
- continue;
- }
- }
-
- $cleaned[$field] = $value;
- }
-
- $this->insert_record_raw($table, $cleaned, false, true, true);
- $id = $dataobject['id'];
-
- foreach ($blobs as $key=>$value) {
- $value = pg_escape_bytea($this->pgsql, $value);
- $sql = "UPDATE {$this->prefix}$table SET $key = '$value'::bytea WHERE id = $id";
- $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
- $result = pg_query($this->pgsql, $sql);
- $this->query_end($result);
- if ($result !== false) {
- pg_free_result($result);
- }
+ $column = $columns[$field];
+ $cleaned[$field] = $this->normalise_value($column, $value);
}
- return true;
+ return $this->insert_record_raw($table, $cleaned, false, true, true);
}
/**
$columns = $this->get_columns($table);
$cleaned = array();
- $blobs = array();
foreach ($dataobject as $field=>$value) {
if (!isset($columns[$field])) {
continue;
}
$column = $columns[$field];
- $normalised_value = $this->normalise_value($column, $value);
- if (is_array($normalised_value) && array_key_exists('blob', $normalised_value)) {
- $cleaned[$field] = '@#BLOB#@';
- $blobs[$field] = $normalised_value['blob'];
- } else {
- $cleaned[$field] = $normalised_value;
- }
+ $cleaned[$field] = $this->normalise_value($column, $value);
}
$this->update_record_raw($table, $cleaned, $bulk);
- if (empty($blobs)) {
- return true;
- }
-
- $id = (int)$dataobject['id'];
-
- foreach ($blobs as $key=>$value) {
- $value = pg_escape_bytea($this->pgsql, $value);
- $sql = "UPDATE {$this->prefix}$table SET $key = '$value'::bytea WHERE id = $id";
- $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
- $result = pg_query($this->pgsql, $sql);
- $this->query_end($result);
-
- pg_free_result($result);
- }
-
return true;
}
$columns = $this->get_columns($table);
$column = $columns[$newfield];
- $normalised_value = $this->normalise_value($column, $newvalue);
- if (is_array($normalised_value) && array_key_exists('blob', $normalised_value)) {
- // Update BYTEA and return
- $normalised_value = pg_escape_bytea($this->pgsql, $normalised_value['blob']);
- $sql = "UPDATE {$this->prefix}$table SET $newfield = '$normalised_value'::bytea $select";
- $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
- $result = pg_query_params($this->pgsql, $sql, $params);
- $this->query_end($result);
- pg_free_result($result);
- return true;
- }
+ $normalisedvalue = $this->normalise_value($column, $newvalue);
- if (is_null($normalised_value)) {
- $newfield = "$newfield = NULL";
- } else {
- $newfield = "$newfield = \$".$i;
- $params[] = $normalised_value;
- }
+ $newfield = "$newfield = \$" . $i;
+ $params[] = $normalisedvalue;
$sql = "UPDATE {$this->prefix}$table SET $newfield $select";
$this->query_start($sql, $params, SQL_QUERY_UPDATE);
}
/**
- * Delete one or more records from a table which match a particular WHERE clause.
+ * Delete one or more records from a table which match a particular WHERE clause, lobs not supported.
*
* @param string $table The database table to be checked against.
* @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
if (strpos($param, '%') !== false) {
debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
}
- if ($escapechar === '\\') {
- // Prevents problems with C-style escapes of enclosing '\',
- // E'... bellow prevents compatibility warnings.
- $escapechar = '\\\\';
- }
// postgresql does not support accent insensitive text comparisons, sorry
if ($casesensitive) {
} else {
$LIKE = $notlike ? 'NOT ILIKE' : 'ILIKE';
}
- return "$fieldname $LIKE $param ESCAPE E'$escapechar'";
+ return "$fieldname $LIKE $param ESCAPE '$escapechar'";
}
public function sql_bitxor($int1, $int2) {
protected $result;
/** @var current row as array.*/
protected $current;
- protected $bytea_oid;
protected $blobs = array();
- public function __construct($result, $bytea_oid) {
- $this->result = $result;
- $this->bytea_oid = $bytea_oid;
-
- // find out if there are any blobs
- $numrows = pg_num_fields($result);
- for($i=0; $i<$numrows; $i++) {
- $type_oid = pg_field_type_oid($result, $i);
- if ($type_oid == $this->bytea_oid) {
+ /**
+ * Build a new recordset to iterate over.
+ *
+ * @param resource $result A pg_query() result object to create a recordset from.
+ */
+ public function __construct($result) {
+ $this->result = $result;
+
+ // Find out if there are any blobs.
+ $numfields = pg_num_fields($result);
+ for ($i = 0; $i < $numfields; $i++) {
+ $type = pg_field_type($result, $i);
+ if ($type == 'bytea') {
$this->blobs[] = pg_field_name($result, $i);
}
}
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
- $DB->insert_record($tablename, array('course' => 1));
- $DB->insert_record($tablename, array('course' => 3));
- $DB->insert_record($tablename, array('course' => 2));
- $DB->insert_record($tablename, array('course' => 6));
+ $binarydata = '\\'.chr(241);
+
+ $DB->insert_record($tablename, array('course' => 1, 'onebinary' => $binarydata));
+ $DB->insert_record($tablename, array('course' => 3, 'onebinary' => $binarydata));
+ $DB->insert_record($tablename, array('course' => 2, 'onebinary' => $binarydata));
+ $DB->insert_record($tablename, array('course' => 6, 'onebinary' => $binarydata));
$fieldset = $DB->get_fieldset_sql("SELECT * FROM {{$tablename}} WHERE course > ?", array(1));
$this->assertInternalType('array', $fieldset);
$this->assertEquals(2, $fieldset[0]);
$this->assertEquals(3, $fieldset[1]);
$this->assertEquals(4, $fieldset[2]);
+
+ $fieldset = $DB->get_fieldset_sql("SELECT onebinary FROM {{$tablename}} WHERE course > ?", array(1));
+ $this->assertInternalType('array', $fieldset);
+
+ $this->assertCount(3, $fieldset);
+ $this->assertEquals($binarydata, $fieldset[0]);
+ $this->assertEquals($binarydata, $fieldset[1]);
+ $this->assertEquals($binarydata, $fieldset[2]);
}
public function test_insert_record_raw() {
$this->assertEquals($clob, $DB->get_field($tablename, 'onetext', array('id' => 1)), 'Test CLOB set_field (full contents output disabled)');
$this->assertEquals($blob, $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Test BLOB set_field (full contents output disabled)');
+ // Empty data in binary columns works.
+ $DB->set_field_select($tablename, 'onebinary', '', 'id = ?', array(1));
+ $this->assertEquals('', $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Blobs need to accept empty values.');
+
// And "small" LOBs too, just in case.
$newclob = substr($clob, 0, 500);
$newblob = substr($blob, 0, 250);
this.hideTimer = Y.later(intTimeout, this, function() {
Y.log('Hide Atto notification.', 'debug', LOGNAME_NOTIFY);
this.hideTimer = null;
- this.messageOverlay.hide(true);
+ if (this.messageOverlay.inDoc()) {
+ this.messageOverlay.hide(true);
+ }
});
return this;
return array($courses, $warnings);
}
+ /**
+ * Returns all area files (optionally limited by itemid).
+ *
+ * @param int $contextid context ID
+ * @param string $component component
+ * @param string $filearea file area
+ * @param int $itemid item ID or all files if not specified
+ * @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it)
+ * @return array of files, compatible with the external_files structure.
+ * @since Moodle 3.2
+ */
+ public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) {
+ $files = array();
+ $fs = get_file_storage();
+
+ if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) {
+ foreach ($areafiles as $areafile) {
+ $file = array();
+ $file['filename'] = $areafile->get_filename();
+ $file['filepath'] = $areafile->get_filepath();
+ $file['mimetype'] = $areafile->get_mimetype();
+ $file['filesize'] = $areafile->get_filesize();
+ $file['timemodified'] = $areafile->get_timemodified();
+ $fileitemid = $useitemidinurl ? $areafile->get_itemid() : null;
+ $file['fileurl'] = moodle_url::make_webservice_pluginfile_url($contextid, $component, $filearea,
+ $fileitemid, $areafile->get_filepath(), $areafile->get_filename())->out(false);
+ $files[] = $file;
+ }
+ }
+ return $files;
+ }
+}
+
+/**
+ * External structure representing a set of files.
+ *
+ * @package core_webservice
+ * @copyright 2016 Juan Leyva
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.2
+ */
+class external_files extends external_multiple_structure {
+
+ /**
+ * Constructor
+ * @param string $desc Description for the multiple structure.
+ * @param int $required The type of value (VALUE_REQUIRED OR VALUE_OPTIONAL).
+ */
+ public function __construct($desc = 'List of files.', $required = VALUE_REQUIRED) {
+
+ parent::__construct(
+ new external_single_structure(
+ array(
+ 'filename' => new external_value(PARAM_FILE, 'File name.', VALUE_OPTIONAL),
+ 'filepath' => new external_value(PARAM_PATH, 'File path.', VALUE_OPTIONAL),
+ 'filesize' => new external_value(PARAM_INT, 'File size.', VALUE_OPTIONAL),
+ 'fileurl' => new external_value(PARAM_URL, 'Downloadable file url.', VALUE_OPTIONAL),
+ 'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL),
+ 'mimetype' => new external_value(PARAM_RAW, 'File mime type.', VALUE_OPTIONAL),
+ ),
+ 'File.'
+ ),
+ $desc,
+ $required
+ );
+ }
}
$options->nocache = true; // temporary workaround for MDL-5136
$text = $pathisstring ? $path : implode('', file($path));
- $text = file_modify_html_header($text);
$output = format_text($text, FORMAT_HTML, $options, $COURSE->id);
readstring_accel($output, $mimetype, false);
$options->noclean = true;
$options->nocache = true; // temporary workaround for MDL-5136
$text = $stored_file->get_content();
- $text = file_modify_html_header($text);
$output = format_text($text, FORMAT_HTML, $options, $COURSE->id);
readstring_accel($output, $mimetype, false);
}
}
-/**
- * add includes (js and css) into uploaded files
- * before returning them, useful for themes and utf.js includes
- *
- * @global stdClass $CFG
- * @param string $text text to search and replace
- * @return string text with added head includes
- * @todo MDL-21120
- */
-function file_modify_html_header($text) {
- // first look for <head> tag
- global $CFG;
-
- $stylesheetshtml = '';
-/*
- foreach ($CFG->stylesheets as $stylesheet) {
- //TODO: MDL-21120
- $stylesheetshtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
- }
-*/
- // TODO The code below is actually a waste of CPU. When MDL-29738 will be implemented it should be re-evaluated too.
-
- preg_match('/\<head\>|\<HEAD\>/', $text, $matches);
- if ($matches) {
- $replacement = '<head>'.$stylesheetshtml;
- $text = preg_replace('/\<head\>|\<HEAD\>/', $replacement, $text, 1);
- return $text;
- }
-
- // if not, look for <html> tag, and stick <head> right after
- preg_match('/\<html\>|\<HTML\>/', $text, $matches);
- if ($matches) {
- // replace <html> tag with <html><head>includes</head>
- $replacement = '<html>'."\n".'<head>'.$stylesheetshtml.'</head>';
- $text = preg_replace('/\<html\>|\<HTML\>/', $replacement, $text, 1);
- return $text;
- }
-
- // if not, look for <body> tag, and stick <head> before body
- preg_match('/\<body\>|\<BODY\>/', $text, $matches);
- if ($matches) {
- $replacement = '<head>'.$stylesheetshtml.'</head>'."\n".'<body>';
- $text = preg_replace('/\<body\>|\<BODY\>/', $replacement, $text, 1);
- return $text;
- }
-
- // if not, just stick a <head> tag at the beginning
- $text = '<head>'.$stylesheetshtml.'</head>'."\n".$text;
- return $text;
-}
-
/**
* Tells whether the filename is executable.
*
public function encodeHeader($str, $position = 'text') {
$encoded = core_text::encode_mimeheader($str, $this->CharSet);
if ($encoded !== false) {
- $encoded = str_replace("\n", $this->LE, $encoded);
if ($position === 'phrase') {
- return ("\"$encoded\"");
+ // Escape special symbols in each line in the encoded string, join back together and enclose in quotes.
+ $chunks = preg_split("/\\n/", $encoded);
+ $chunks = array_map(function($chunk) {
+ return addcslashes($chunk, "\0..\37\177\\\"");
+ }, $chunks);
+ return '"' . join($this->LE, $chunks) . '"';
}
- return $encoded;
+ return str_replace("\n", $this->LE, $encoded);
}
return parent::encodeHeader($str, $position);
if (CLI_SCRIPT) {
// sometimes people use different PHP binary for web and CLI, make 100% sure they have the supported PHP version
- if (version_compare(phpversion(), '5.4.4') < 0) {
+ if (version_compare(phpversion(), '5.6.5') < 0) {
$phpversion = phpversion();
// do NOT localise - lang strings would not work here and we CAN NOT move it to later place
- echo "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).\n";
+ echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n";
echo "Some servers may have multiple PHP versions installed, are you using the correct executable?\n";
exit(1);
}
*/
protected static $timings = array();
+ /**
+ * Hook to capture BeforeSuite event so as to give access to moodle codebase.
+ * This will try and catch any exception and exists if anything fails.
+ *
+ * @param BeforeSuiteScope $scope scope passed by event fired before suite.
+ * @BeforeSuite
+ */
+ public static function before_suite_hook(BeforeSuiteScope $scope) {
+ try {
+ self::before_suite($scope);
+ } catch (behat_stop_exception $e) {
+ echo $e->getMessage() . PHP_EOL;
+ exit(1);
+ }
+ }
+
/**
* Gives access to moodle codebase, ensures all is ready and sets up the test lock.
*
* Includes config.php to use moodle codebase with $CFG->behat_*
* instead of $CFG->prefix and $CFG->dataroot, called once per suite.
*
- * @param SuiteEvent $event event before suite.
+ * @param BeforeSuiteScope $scope scope passed by event fired before suite.
* @static
- * @throws Exception
- * @BeforeSuite
+ * @throws behat_stop_exception
*/
public static function before_suite(BeforeSuiteScope $scope) {
global $CFG;
// before each scenario (accidental user deletes) in the BeforeScenario hook.
if (!behat_util::is_test_mode_enabled()) {
- throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
+ throw new behat_stop_exception('Behat only can run if test mode is enabled. More info in ' .
+ behat_command::DOCS_URL . '#Running_tests');
}
// Reset all data, before checking for check_server_status.
// Prevents using outdated data, upgrade script would start and tests would fail.
if (!behat_util::is_test_data_updated()) {
$commandpath = 'php admin/tool/behat/cli/init.php';
- throw new Exception("Your behat test site is outdated, please run\n\n " .
+ throw new behat_stop_exception("Your behat test site is outdated, please run\n\n " .
$commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
}
// Avoid parallel tests execution, it continues when the previous lock is released.
}
if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
- throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory');
+ throw new behat_stop_exception('You set $CFG->behat_faildump_path to a non-writable directory');
}
// Handle interrupts on PHP7.
/**
* Gives access to moodle codebase, to keep track of feature start time.
*
- * @param FeatureEvent $event event fired before feature.
+ * @param BeforeFeatureScope $scope scope passed by event fired before feature.
* @BeforeFeature
*/
- public static function before_feature(BeforeFeatureScope $event) {
+ public static function before_feature(BeforeFeatureScope $scope) {
if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
return;
}
- $file = $event->getFeature()->getFile();
+ $file = $scope->getFeature()->getFile();
self::$timings[$file] = microtime(true);
}
/**
* Gives access to moodle codebase, to keep track of feature end time.
*
- * @param FeatureEvent $event event fired after feature.
+ * @param AfterFeatureScope $scope scope passed by event fired after feature.
* @AfterFeature
*/
- public static function after_feature(AfterFeatureScope $event) {
+ public static function after_feature(AfterFeatureScope $scope) {
if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
return;
}
- $file = $event->getFeature()->getFile();
+ $file = $scope->getFeature()->getFile();
self::$timings[$file] = microtime(true) - self::$timings[$file];
// Probably didn't actually run this, don't output it.
if (self::$timings[$file] < 1) {
/**
* Gives access to moodle codebase, to keep track of suite timings.
*
- * @param SuiteEvent $event event fired after suite.
+ * @param AfterSuiteScope $scope scope passed by event fired after suite.
* @AfterSuite
*/
- public static function after_suite(AfterSuiteScope $event) {
+ public static function after_suite(AfterSuiteScope $scope) {
if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
return;
}
}
/**
- * Resets the test environment.
+ * Hook to capture before scenario event to get scope.
*
- * @param OutlineExampleEvent|ScenarioEvent $event event fired before scenario.
- * @throws coding_exception If here we are not using the test database it should be because of a coding error
+ * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
* @BeforeScenario
*/
+ public function before_scenario_hook(BeforeScenarioScope $scope) {
+ try {
+ $this->before_scenario($scope);
+ } catch (behat_stop_exception $e) {
+ echo $e->getMessage() . PHP_EOL;
+ exit(1);
+ }
+ }
+
+ /**
+ * Resets the test environment.
+ *
+ * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
+ * @throws behat_stop_exception If here we are not using the test database it should be because of a coding error
+ */
public function before_scenario(BeforeScenarioScope $scope) {
global $DB, $SESSION, $CFG;
php_sapi_name() != 'cli' ||
!behat_util::is_test_mode_enabled() ||
!behat_util::is_test_site()) {
- throw new coding_exception('Behat only can modify the test database and the test dataroot!');
+ throw new behat_stop_exception('Behat only can modify the test database and the test dataroot!');
}
$moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
} catch (CurlExec $e) {
// Exception thrown by WebDriver, so only @javascript tests will be caugth; in
// behat_util::check_server_status() we already checked that the server is running.
- $this->stop_execution($driverexceptionmsg);
+ throw new behat_stop_exception($driverexceptionmsg);
} catch (DriverException $e) {
- $this->stop_execution($driverexceptionmsg);
+ throw new behat_stop_exception($driverexceptionmsg);
} catch (UnknownError $e) {
// Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
- $this->stop_execution($e->getMessage());
+ throw new behat_stop_exception($e->getMessage());
}
// We need the Mink session to do it and we do it only before the first scenario.
// Let's be conservative as we never know when new upstream issues will affect us.
$session->visit($this->locate_path('/'));
} catch (UnknownError $e) {
- $this->stop_execution($e->getMessage());
+ throw new behat_stop_exception($e->getMessage());
}
// Checking that the root path is a Moodle test site.
if (self::is_first_scenario()) {
- $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
+ $notestsiteexception = new behat_stop_exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
$this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
* default would be at framework level, which will stop the execution of
* the run.
*
+ * @param BeforeStepScope $scope scope passed by event fired before step.
* @BeforeStep
*/
public function before_step_javascript(BeforeStepScope $scope) {
* default would be at framework level, which will stop the execution of
* the run.
*
+ * @param AfterStepScope $scope scope passed by event fired after step..
* @AfterStep
*/
public function after_step_javascript(AfterStepScope $scope) {
* This is needed to close all extra browser windows and starting
* one browser window.
*
- * @param AfterScenarioScope $event event fired after scenario.
+ * @param AfterScenarioScope $scope scope passed by event fired after scenario.
* @AfterScenario @_switch_window
*/
- public function after_scenario_switchwindow(AfterScenarioScope $event) {
+ public function after_scenario_switchwindow(AfterScenarioScope $scope) {
for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count++) {
try {
$this->getSession()->restart();
* Take screenshot when a step fails.
*
* @throws Exception
- * @param AfterStepScope $scope
+ * @param AfterStepScope $scope scope passed by event after step.
*/
protected function take_screenshot(AfterStepScope $scope) {
// Goutte can't save screenshots.
* Take a dump of the page content when a step fails.
*
* @throws Exception
- * @param AfterStepScope $scope
+ * @param AfterStepScope $scope scope passed by event after step.
*/
protected function take_contentdump(AfterStepScope $scope) {
list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
*
* This is used for content such as the DOM, and screenshots.
*
- * @param AfterStepScope $scope
+ * @param AfterStepScope $scope scope passed by event after step.
* @param String $filetype The file suffix to use. Limited to 4 chars.
*/
protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
protected static function is_first_scenario() {
return !(self::$initprocessesfinished);
}
-
- /**
- * Stops execution because of some exception.
- *
- * @param string $exception
- * @return void
- */
- protected function stop_execution($exception) {
- $text = get_string('unknownexceptioninfo', 'tool_behat');
- echo $text . PHP_EOL . $exception . PHP_EOL;
- exit(1);
- }
-
}
+/**
+ * Behat stop exception
+ *
+ * This exception is thrown from before suite or scenario if any setup problem found.
+ *
+ * @package core_test
+ * @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_stop_exception extends \Exception{}
\ No newline at end of file
class core_externallib_testcase extends advanced_testcase {
+ protected $DB;
+
+ public function setUp() {
+ $this->DB = null;
+ }
+
+ public function tearDown() {
+ global $DB;
+ if ($this->DB !== null) {
+ $DB = $this->DB;
+ }
+ }
+
public function test_validate_params() {
$params = array('text'=>'aaa', 'someid'=>'6');
$description = new external_function_parameters(array('someid' => new external_value(PARAM_INT, 'Some int value'),
$this->assertSame($beforecourse, $COURSE);
}
+ /**
+ * Text external_util::get_area_files
+ */
+ public function test_external_util_get_area_files() {
+ global $CFG, $DB;
+
+ $this->DB = $DB;
+ $DB = $this->getMockBuilder('moodle_database')->getMock();
+
+ $content = base64_encode("Let us create a nice simple file.");
+ $timemodified = 102030405;
+ $itemid = 42;
+ $filesize = strlen($content);
+
+ $DB->method('get_records_sql')->willReturn([
+ (object) [
+ 'filename' => 'example.txt',
+ 'filepath' => '/',
+ 'mimetype' => 'text/plain',
+ 'filesize' => $filesize,
+ 'timemodified' => $timemodified,
+ 'itemid' => $itemid,
+ 'pathnamehash' => sha1('/example.txt'),
+ ],
+ ]);
+
+ $component = 'mod_foo';
+ $filearea = 'area';
+ $context = 12345;
+
+ $expectedfiles[] = array(
+ 'filename' => 'example.txt',
+ 'filepath' => '/',
+ 'fileurl' => "{$CFG->wwwroot}/webservice/pluginfile.php/{$context}/{$component}/{$filearea}/{$itemid}/example.txt",
+ 'timemodified' => $timemodified,
+ 'filesize' => $filesize,
+ 'mimetype' => 'text/plain',
+ );
+ // Get all the files for the area.
+ $files = external_util::get_area_files($context, $component, $filearea, false);
+ $this->assertEquals($expectedfiles, $files);
+
+ // Get just the file indicated by $itemid.
+ $files = external_util::get_area_files($context, $component, $filearea, $itemid);
+ $this->assertEquals($expectedfiles, $files);
+
+ }
+
+ /**
+ * Text external files structure.
+ */
+ public function test_external_files() {
+
+ $description = new external_files();
+
+ // First check that the expected default values and keys are returned.
+ $expectedkeys = array_flip(array('filename', 'filepath', 'filesize', 'fileurl', 'timemodified', 'mimetype'));
+ $returnedkeys = array_flip(array_keys($description->content->keys));
+ $this->assertEquals($expectedkeys, $returnedkeys);
+ $this->assertEquals('List of files.', $description->desc);
+ $this->assertEquals(VALUE_REQUIRED, $description->required);
+ foreach ($description->content->keys as $key) {
+ $this->assertEquals(VALUE_OPTIONAL, $key->required);
+ }
+
+ }
+
}
/*
/**
* Tests the static encode_mimeheader method.
+ * This also tests method moodle_phpmailer::encodeHeader that calls core_text::encode_mimeheader
*/
public function test_encode_mimeheader() {
+ global $CFG;
+ require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
+ $mailer = new moodle_phpmailer();
+
+ // Encode short string with non-latin characters.
$str = "Žluťoučký koníček";
- $this->assertSame('=?utf-8?B?xb1sdcWlb3XEjWvDvSBrb27DrcSNZWs=?=', core_text::encode_mimeheader($str));
+ $encodedstr = '=?utf-8?B?xb1sdcWlb3XEjWvDvSBrb27DrcSNZWs=?=';
+ $this->assertSame($encodedstr, core_text::encode_mimeheader($str));
+ $this->assertSame($encodedstr, $mailer->encodeHeader($str));
+ $this->assertSame('"' . $encodedstr . '"', $mailer->encodeHeader($str, 'phrase'));
+
+ // Encode short string without non-latin characters. Make sure the quotes are escaped in quoted email headers.
+ $latinstr = 'text"with quotes';
+ $this->assertSame($latinstr, core_text::encode_mimeheader($latinstr));
+ $this->assertSame($latinstr, $mailer->encodeHeader($latinstr));
+ $this->assertSame('"text\\"with quotes"', $mailer->encodeHeader($latinstr, 'phrase'));
+
+ // Encode long string without non-latin characters.
+ $longlatinstr = 'This is a very long text that still should not be split into several lines in the email headers because '.
+ 'it does not have any non-latin characters. The "quotes" and \\backslashes should be escaped only if it\'s a part of email address';
+ $this->assertSame($longlatinstr, core_text::encode_mimeheader($longlatinstr));
+ $this->assertSame($longlatinstr, $mailer->encodeHeader($longlatinstr));
+ $longlatinstrwithslash = preg_replace(['/\\\\/', "/\"/"], ['\\\\\\', '\\"'], $longlatinstr);
+ $this->assertSame('"' . $longlatinstrwithslash . '"', $mailer->encodeHeader($longlatinstr, 'phrase'));
+
+ // Encode long string with non-latin characters.
+ $longstr = "Неопознанная ошибка в файле C:\\tmp\\: \"Не пользуйтесь виндоуз\"";
+ $encodedlongstr = "=?utf-8?B?0J3QtdC+0L/QvtC30L3QsNC90L3QsNGPINC+0YjQuNCx0LrQsCDQsiDRhNCw?=
+ =?utf-8?B?0LnQu9C1IEM6XHRtcFw6ICLQndC1INC/0L7Qu9GM0LfRg9C50YLQtdGB?=
+ =?utf-8?B?0Ywg0LLQuNC90LTQvtGD0Lci?=";
+ $this->assertSame($encodedlongstr, $mailer->encodeHeader($longstr));
+ $this->assertSame('"' . $encodedlongstr . '"', $mailer->encodeHeader($longstr, 'phrase'));
}
/**
$codeman = new \core\update\testable_code_manager();
$zipfilepath = __DIR__.'/fixtures/update_validator/zips/invalidroot.zip';
$targetdir = make_request_directory();
+ mkdir($targetdir.'/aaa_another');
$files = $codeman->unzip_plugin_file($zipfilepath, $targetdir);
$files = $codeman->unzip_plugin_file($zipfilepath, $targetdir, 'bar');
}
+ public function test_unzip_plugin_file_multidir() {
+ $codeman = new \core\update\testable_code_manager();
+ $zipfilepath = __DIR__.'/fixtures/update_validator/zips/multidir.zip';
+ $targetdir = make_request_directory();
+ // Attempting to rename the root folder if there are multiple ones should lead to exception.
+ $this->setExpectedException('moodle_exception');
+ $files = $codeman->unzip_plugin_file($zipfilepath, $targetdir, 'foo');
+ }
+
public function test_get_plugin_zip_root_dir() {
$codeman = new \core\update\testable_code_manager();
$zipfilepath = __DIR__.'/fixtures/update_validator/zips/bar.zip';
$this->assertEquals('bar', $codeman->get_plugin_zip_root_dir($zipfilepath));
+
+ $zipfilepath = __DIR__.'/fixtures/update_validator/zips/multidir.zip';
+ $this->assertSame(false, $codeman->get_plugin_zip_root_dir($zipfilepath));
}
public function test_list_plugin_folder_files() {
$this->setExpectedException('coding_exception', 'Invalid property requested, or the property does not has a default value.');
core_user::get_property_default('firstname');
}
+
+ /**
+ * Ensure that the noreply user is not cached.
+ */
+ public function test_get_noreply_user() {
+ global $CFG;
+
+ // Create a new fake language 'xx' with the 'noreplyname'.
+ $langfolder = $CFG->dataroot . '/lang/xx';
+ check_dir_exists($langfolder);
+ $langconfig = "<?php\n\defined('MOODLE_INTERNAL') || die();";
+ file_put_contents($langfolder . '/langconfig.php', $langconfig);
+ $langconfig = "<?php\n\$string['noreplyname'] = 'XXX';";
+ file_put_contents($langfolder . '/moodle.php', $langconfig);
+
+ $CFG->lang='en';
+ $enuser = \core_user::get_noreply_user();
+
+ $CFG->lang='xx';
+ $xxuser = \core_user::get_noreply_user();
+
+ $this->assertNotEquals($enuser, $xxuser);
+ }
+
}
=== 3.2 ===
* New option 'blanktarget' added to format_text. This option adds target="_blank" to links
+* A new webservice structure `external_files` has been created which provides a standardised view of files in Moodle and
+ should be used for all file return descriptions.
+ Files matching this format can be retrieved via the new `external_util::get_area_files` method.
+ See MDL-54951 for further information.
* The parameter $usepost of the following functions has been deprecated and is not used any more:
- get_max_upload_file_size()
- get_user_max_upload_file_size()
+* The following functions have been removed and should not be used any more:
+ - file_modify_html_header() - See MDL-29738 for more information.
=== 3.1 ===
// table parameter is ignored
$name = $this->getName();
if (strlen($name) > self::NAME_MAX_LENGTH) {
- return 'Invalid table name {'.$name.'}: name is too long. Limit is 28 chars.';
+ return 'Invalid table name {'.$name.'}: name is too long. Limit is '.self::NAME_MAX_LENGTH.' chars.';
}
if (!preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
return 'Invalid table name {'.$name.'}: name includes invalid characters.';
* @return true if ok, false if error
*/
public function send_message($eventdata) {
- global $CFG;
+ global $CFG, $DB;
require_once($CFG->libdir . '/filelib.php');
if (!empty($CFG->noemailever)) {
return true;
}
+ // If username is empty we try to retrieve it, since it's required to generate the siteid.
+ if (empty($eventdata->userto->username)) {
+ $eventdata->userto->username = $DB->get_field('user', 'username', array('id' => $eventdata->userto->id));
+ }
+
// Site id, to map with Moodle Mobile stored sites.
$siteid = md5($CFG->wwwroot . $eventdata->userto->username);
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
-define(['jquery'], function($) {
+define(['jquery', 'mod_assign/grading_events'], function($, GradingEvents) {
/**
* GradingActions class.
this._regionSelector = selector;
this._region = $(selector);
- $(document).on('user-changed', this._showActionsForm.bind(this));
-
- this._region.find('[name="savechanges"]').on('click', this._trigger.bind(this, 'save-changes'));
- this._region.find('[name="resetbutton"]').on('click', this._trigger.bind(this, 'reset'));
- this._region.find('form').on('submit', function(e) {
- e.preventDefault();
- });
+ this.registerEventListeners();
};
/** @type {String} Selector for the page region containing the user navigation. */
$(document).trigger(action);
};
+ /**
+ * Get the review panel element.
+ *
+ * @method getReviewPanelElement
+ * @return {jQuery}
+ */
+ GradingActions.prototype.getReviewPanelElement = function() {
+ return $('[data-region="review-panel"]');
+ };
+
+ /**
+ * Check if the page has a review panel.
+ *
+ * @method hasReviewPanelElement
+ * @return {bool}
+ */
+ GradingActions.prototype.hasReviewPanelElement = function() {
+ return this.getReviewPanelElement().length > 0;
+ };
+
+ /**
+ * Get the collapse grade panel button.
+ *
+ * @method getCollapseGradePanelButton
+ * @return {jQuery}
+ */
+ GradingActions.prototype.getCollapseGradePanelButton = function() {
+ return $('[data-region="grade-actions"] .collapse-grade-panel');
+ };
+
+ /**
+ * Get the collapse review panel button.
+ *
+ * @method getCollapseReviewPanelButton
+ * @return {jQuery}
+ */
+ GradingActions.prototype.getCollapseReviewPanelButton = function() {
+ return $('[data-region="grade-actions"] .collapse-review-panel');
+ };
+
+ /**
+ * Get the expand all panels button.
+ *
+ * @method getExpandAllPanelsButton
+ * @return {jQuery}
+ */
+ GradingActions.prototype.getExpandAllPanelsButton = function() {
+ return $('[data-region="grade-actions"] .collapse-none');
+ };
+
+ /**
+ * Remove the active state from all layout buttons.
+ *
+ * @method resetLayoutButtons
+ */
+ GradingActions.prototype.resetLayoutButtons = function() {
+ this.getCollapseGradePanelButton().removeClass('active');
+ this.getCollapseReviewPanelButton().removeClass('active');
+ this.getExpandAllPanelsButton().removeClass('active');
+ };
+
+ /**
+ * Hide the review panel.
+ *
+ * @method collapseReviewPanel
+ */
+ GradingActions.prototype.collapseReviewPanel = function() {
+ $(document).trigger(GradingEvents.COLLAPSE_REVIEW_PANEL);
+ $(document).trigger(GradingEvents.EXPAND_GRADE_PANEL);
+ this.resetLayoutButtons();
+ this.getCollapseReviewPanelButton().addClass('active');
+ };
+
+ /**
+ * Show/Hide the grade panel.
+ *
+ * @method collapseGradePanel
+ */
+ GradingActions.prototype.collapseGradePanel = function() {
+ $(document).trigger(GradingEvents.COLLAPSE_GRADE_PANEL);
+ $(document).trigger(GradingEvents.EXPAND_REVIEW_PANEL);
+ this.resetLayoutButtons();
+ this.getCollapseGradePanelButton().addClass('active');
+ };
+
+ /**
+ * Return the layout to default.
+ *
+ * @method expandAllPanels
+ */
+ GradingActions.prototype.expandAllPanels = function() {
+ $(document).trigger(GradingEvents.EXPAND_GRADE_PANEL);
+ $(document).trigger(GradingEvents.EXPAND_REVIEW_PANEL);
+ this.resetLayoutButtons();
+ this.getExpandAllPanelsButton().addClass('active');
+ };
+
+ /**
+ * Register event listeners for the grade panel.
+ *
+ * @method registerEventListeners
+ */
+ GradingActions.prototype.registerEventListeners = function() {
+ // Don't need layout controls if there is no review panel.
+ if (this.hasReviewPanelElement()) {
+ var collapseReviewPanelButton = this.getCollapseReviewPanelButton();
+ collapseReviewPanelButton.click(function(e) {
+ this.collapseReviewPanel();
+ e.preventDefault();
+ }.bind(this));
+
+ collapseReviewPanelButton.keydown(function(e) {
+ if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ this.collapseReviewPanel();
+ e.preventDefault();
+ }
+ }
+ }.bind(this));
+
+ var collapseGradePanelButton = this.getCollapseGradePanelButton();
+ collapseGradePanelButton.click(function(e) {
+ this.collapseGradePanel();
+ e.preventDefault();
+ }.bind(this));
+
+ collapseGradePanelButton.keydown(function(e) {
+ if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ this.collapseGradePanel();
+ e.preventDefault();
+ }
+ }
+ }.bind(this));
+
+ var expandAllPanelsButton = this.getExpandAllPanelsButton();
+ expandAllPanelsButton.click(function(e) {
+ this.expandAllPanels();
+ e.preventDefault();
+ }.bind(this));
+
+ expandAllPanelsButton.keydown(function(e) {
+ if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ this.expandAllPanels();
+ e.preventDefault();
+ }
+ }
+ }.bind(this));
+ }
+
+ $(document).on('user-changed', this._showActionsForm.bind(this));
+
+ this._region.find('[name="savechanges"]').on('click', this._trigger.bind(this, 'save-changes'));
+ this._region.find('[name="resetbutton"]').on('click', this._trigger.bind(this, 'reset'));
+ this._region.find('form').on('submit', function(e) {
+ e.preventDefault();
+ });
+ };
+
return GradingActions;
});
--- /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/>.
+
+/**
+ * Events for the grading interface.
+ *
+ * @module mod_assign/grading_events
+ * @package mod_assign
+ * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 3.1
+ */
+define(function() {
+ return {
+ COLLAPSE_REVIEW_PANEL: 'grading:collapse-review-panel',
+ EXPAND_REVIEW_PANEL: 'grading:expand-review-panel',
+ COLLAPSE_GRADE_PANEL: 'grading:collapse-grade-panel',
+ EXPAND_GRADE_PANEL: 'grading:expand-grade-panel',
+ };
+});
* @since 3.1
*/
define(['jquery', 'core/notification', 'core/templates', 'core/fragment',
- 'core/ajax', 'core/str', 'mod_assign/grading_form_change_checker'],
- function($, notification, templates, fragment, ajax, str, checker) {
+ 'core/ajax', 'core/str', 'mod_assign/grading_form_change_checker',
+ 'mod_assign/grading_events'],
+ function($, notification, templates, fragment, ajax, str, checker, GradingEvents) {
/**
* GradingPanel class.
this._region = $(selector);
this._userCache = [];
- $(document).on('user-changed', this._refreshGradingPanel.bind(this));
- $(document).on('save-changes', this._submitForm.bind(this));
- $(document).on('reset', this._resetForm.bind(this));
-
- $(document).on('save-form-state', this._saveFormState.bind(this));
+ this.registerEventListeners();
};
/** @type {String} Selector for the page region containing the user navigation. */
}.bind(this)).fail(notification.exception);
};
+ /**
+ * Get the grade panel element.
+ *
+ * @method getPanelElement
+ * @return {jQuery}
+ */
+ GradingPanel.prototype.getPanelElement = function() {
+ return $('[data-region="grade-panel"]');
+ };
+
+ /**
+ * Hide the grade panel.
+ *
+ * @method collapsePanel
+ */
+ GradingPanel.prototype.collapsePanel = function() {
+ this.getPanelElement().addClass('collapsed');
+ };
+
+ /**
+ * Show the grade panel.
+ *
+ * @method expandPanel
+ */
+ GradingPanel.prototype.expandPanel = function() {
+ this.getPanelElement().removeClass('collapsed');
+ };
+
+ /**
+ * Register event listeners for the grade panel.
+ *
+ * @method registerEventListeners
+ */
+ GradingPanel.prototype.registerEventListeners = function() {
+ var docElement = $(document);
+
+ docElement.on('user-changed', this._refreshGradingPanel.bind(this));
+ docElement.on('save-changes', this._submitForm.bind(this));
+ docElement.on('reset', this._resetForm.bind(this));
+
+ docElement.on('save-form-state', this._saveFormState.bind(this));
+
+ docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL, function() {
+ this.collapsePanel();
+ }.bind(this));
+
+ // We should expand if the review panel is collapsed.
+ docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL, function() {
+ this.expandPanel();
+ }.bind(this));
+
+ docElement.on(GradingEvents.EXPAND_GRADE_PANEL, function() {
+ this.expandPanel();
+ }.bind(this));
+ };
+
return GradingPanel;
});
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
-define(['jquery'], function($) {
+define(['jquery', 'mod_assign/grading_events'], function($, GradingEvents) {
/**
* GradingReviewPanel class.
* @param {String} selector The selector for the page region containing the user navigation.
*/
var GradingReviewPanel = function() {
- this._region = $('[data-region="review-panel"]');
+ this._region = $('[data-region="review-panel-content"]');
+ this.registerEventListeners();
};
/** @type {JQuery} JQuery node for the page region containing the user navigation. */
return false;
};
+ /**
+ * Get the toggle review panel button.
+ *
+ * @method getTogglePanelButton
+ * @return {jQuery}
+ */
+ GradingReviewPanel.prototype.getTogglePanelButton = function() {
+ return this.getPanelElement().find('[data-region="review-panel-toggle"]');
+ };
+
+ /**
+ * Get the review panel element.
+ *
+ * @method getPanelElement
+ * @return {jQuery}
+ */
+ GradingReviewPanel.prototype.getPanelElement = function() {
+ return $('[data-region="review-panel"]');
+ };
+
+ /**
+ * Get the review panel content element.
+ *
+ * @method getPanelContentElement
+ * @return {jQuery}
+ */
+ GradingReviewPanel.prototype.getPanelContentElement = function() {
+ return $('[data-region="review-panel-content"]');
+ };
+
+ /**
+ * Show/Hide the review panel.
+ *
+ * @method togglePanel
+ */
+ GradingReviewPanel.prototype.togglePanel = function() {
+ if (this.getPanelElement().hasClass('collapsed')) {
+ $(document).trigger(GradingEvents.EXPAND_REVIEW_PANEL);
+ } else {
+ $(document).trigger(GradingEvents.COLLAPSE_REVIEW_PANEL);
+ }
+ };
+
+ /**
+ * Hide the review panel.
+ *
+ * @method collapsePanel
+ */
+ GradingReviewPanel.prototype.collapsePanel = function() {
+ this.getPanelElement().addClass('collapsed').removeClass('grade-panel-collapsed');
+ this.getPanelContentElement().attr('aria-hidden', true);
+ };
+
+ /**
+ * Show the review panel.
+ *
+ * @method expandPanel
+ */
+ GradingReviewPanel.prototype.expandPanel = function() {
+ this.getPanelElement().removeClass('collapsed');
+ this.getPanelContentElement().removeAttr('aria-hidden');
+ };
+
+ /**
+ * Register event listeners for the review panel.
+ *
+ * @method registerEventListeners
+ */
+ GradingReviewPanel.prototype.registerEventListeners = function() {
+ var toggleReviewPanelButton = this.getTogglePanelButton();
+ toggleReviewPanelButton.click(function(e) {
+ this.togglePanel();
+ e.preventDefault();
+ }.bind(this));
+
+ toggleReviewPanelButton.keydown(function(e) {
+ if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ this.togglePanel();
+ e.preventDefault();
+ }
+ }
+ }.bind(this));
+
+ var docElement = $(document);
+ docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL, function() {
+ this.collapsePanel();
+ }.bind(this));
+
+ // Need special styling when grade panel is collapsed.
+ docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL, function() {
+ this.expandPanel();
+ this.getPanelElement().addClass('grade-panel-collapsed');
+ }.bind(this));
+
+ docElement.on(GradingEvents.EXPAND_REVIEW_PANEL, function() {
+ this.expandPanel();
+ }.bind(this));
+
+ docElement.on(GradingEvents.EXPAND_GRADE_PANEL, function() {
+ this.getPanelElement().removeClass('grade-panel-collapsed');
+ }.bind(this));
+ };
+
return GradingReviewPanel;
});
'description' => 'List the participants for a single assignment, with some summary info about their submissions.',
'type' => 'read',
'ajax' => true,
- 'capabilities' => 'mod/assign:view, mod/assign:viewgrades'
+ 'capabilities' => 'mod/assign:view, mod/assign:viewgrades',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_assign_submit_grading_form' => array(
if (!isset($courses[$cid])) {
$courses[$cid] = get_course($cid);
}
+ $courses[$cid]->contextid = $context->id;
} catch (Exception $e) {
unset($courses[$cid]);
$warnings[] = array(
list($assignment['intro'], $assignment['introformat']) = external_format_text($module->intro,
$module->introformat, $context->id, 'mod_assign', 'intro', null);
+ $assignment['introfiles'] = external_util::get_area_files($context->id, 'mod_assign', 'intro', false,
+ false);
$fs = get_file_storage();
if ($files = $fs->get_area_files($context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
}
$coursearray[]= array(
'id' => $courses[$id]->id,
- 'fullname' => $courses[$id]->fullname,
- 'shortname' => $courses[$id]->shortname,
+ 'fullname' => external_format_string($courses[$id]->fullname, $course->contextid),
+ 'shortname' => external_format_string($courses[$id]->shortname, $course->contextid),
'timemodified' => $courses[$id]->timemodified,
'assignments' => $assignmentarray
);
'intro' => new external_value(PARAM_RAW,
'assignment intro, not allways returned because it deppends on the activity configuration', VALUE_OPTIONAL),
'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
+ 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
'introattachments' => new external_multiple_structure(
new external_single_structure(
array (
$string['choosegradingaction'] = 'Grading action';
$string['choosemarker'] = 'Choose...';
$string['chooseoperation'] = 'Choose operation';
+$string['clickexpandreviewpanel'] = 'Click to expand review panel';
+$string['collapsegradepanel'] = 'Collapse grade panel';
+$string['collapsereviewpanel'] = 'Collapse review panel';
$string['comment'] = 'Comment';
$string['completionsubmit'] = 'Student must submit to this activity to complete it';
$string['conversionexception'] = 'Could not convert assignment. Exception was: {$a}.';
$string['cutoffdate_help'] = 'If set, the assignment will not accept submissions after this date without an extension.';
$string['cutoffdatevalidation'] = 'The cut-off date cannot be earlier than the due date.';
$string['cutoffdatefromdatevalidation'] = 'Cut-off date must be after the allow submissions from date.';
+$string['defaultlayout'] = 'Restore default layout';
$string['defaultsettings'] = 'Default assignment settings';
$string['defaultsettings_help'] = 'These settings define the defaults for all new assignments.';
$string['defaultteam'] = 'Default group';
$string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date but any assignments submitted after this date are marked as late. To prevent submissions after a certain date - set the assignment cut off date.';
$string['duedateno'] = 'No due date';
$string['submissionempty'] = 'Nothing was submitted';
+$string['submissionmodified'] = 'You have existing submission data. Please leave this page and try again.';
+$string['submissionmodifiedgroup'] = 'The submission has been modified by somebody else. Please leave this page and try again.';
$string['duedatereached'] = 'The due date for this assignment has now passed';
$string['duedatevalidation'] = 'Due date must be after the allow submissions from date.';
$string['editattemptfeedback'] = 'Edit the grade and feedback for attempt number {$a}.';
$string['eventsubmissionupdated'] = 'Submission updated.';
$string['eventsubmissionviewed'] = 'Submission viewed.';
$string['eventworkflowstateupdated'] = 'The state of the workflow has been updated.';
+$string['expandreviewpanel'] = 'Expand review panel';
$string['extensionduedate'] = 'Extension due date';
$string['extensionnotafterduedate'] = 'Extension date must be after the due date';
$string['extensionnotafterfromdate'] = 'Extension date must be after the allow submissions from date';
return $allempty;
}
+ /**
+ * Determine if a new submission is empty or not
+ *
+ * @param stdClass $data Submission data
+ * @return bool
+ */
+ public function new_submission_empty($data) {
+ foreach ($this->submissionplugins as $plugin) {
+ if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
+ !$plugin->submission_is_empty($data)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Save assignment submission for the current user.
*
} else {
$submission = $this->get_user_submission($userid, true);
}
+
+ if ($this->new_submission_empty($data)) {
+ $notices[] = get_string('submissionempty', 'mod_assign');
+ return false;
+ }
+
+ // Check that no one has modified the submission since we started looking at it.
+ if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
+ // Another user has submitted something. Notify the current user.
+ if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
+ $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
+ : get_string('submissionmodified', 'mod_assign');
+ return false;
+ }
+ }
+
if ($instance->submissiondrafts) {
$submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
} else {
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+ x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"\r
+ xml:space="preserve">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M0,0v16h16V0H0z M2,14V2h6v12H2z M14,14h-4V2h4V14z"/>\r
+</svg>\r
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+ x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"\r
+ xml:space="preserve">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M0,0v16h16V0H0z M2,14V2h8v12H2z M14,14h-2V2h2V14z"/>\r
+</svg>\r
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+ x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"\r
+ xml:space="preserve">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M0,0v16h16V0H0z M2,14V2h2v12H2z M14,14H6V2h8V14z"/>\r
+</svg>\r
top: 85px;
bottom: 60px;
left: 0;
- width: 70%;
+ right: 30%;
+ width: auto;
box-sizing: border-box;
+ -webkit-transition: right 0.5s, left 0.5s;
+ -moz-transition: right 0.5s, left 0.5s;
+ transition: right 0.5s, left 0.5s;
+}
+
+.path-mod-assign [data-region="review-panel"].grade-panel-collapsed {
+ right: 30px;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] {
+ display: none;
+ height: 30px;
+ width: 30px;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: auto;
+ box-sizing: border-box;
+ border-radius: 0 0 0 4px;
+ border: 1px solid #cccccc;
+ border-top: none;
+ background-color: #fff;
+ z-index: 99999;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon,
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon {
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ line-height: 30px;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .toggle-text,
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon .toggle-text {
+ visibility: hidden;
+ opacity: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] img {
+ height: 100%;
+ float: right;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon {
+ display: none;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon {
+ display: block;
+}
+
+.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon.full-width {
+ display: none;
}
.path-mod-assign [data-region="review-panel"] .pageheader {
border-right: 1px solid #ddd;
}
+.path-mod-assign [data-region="review-panel"] + [data-region="grade-panel"] [data-region="grade"] {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 100%;
+ -webkit-transition: max-width 0.5s;
+ -moz-transition: max-width 0.5s;
+ transition: max-width 0.5s;
+}
.path-mod-assign [data-region="review-panel"] .drawingregion {
left: 0;
border-color: #ddd;
}
-.path-mod-assign [data-region="grade-panel"].fullwidth {
+.path-mod-assign [data-region="review-panel"].collapsed {
+ left: calc(30px - 70%);
+ right: calc(100% - 30px);
+ -webkit-transition: right 0.5s, left 0.5s;
+ -moz-transition: right 0.5s, left 0.5s;
+ transition: right 0.5s, left 0.5s;
+}
+
+.path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-content"] {
+ visibility: hidden;
+ -webkit-transition: visibility 0s 0.5s;
+ -moz-transition: visibility 0s 0.5s;
+ transition: visibility 0s 0.5s;
+}
+
+.path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .expand-icon {
+ display: block;
+}
+
+.path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .collapse-icon {
+ display: none;
+}
+
+.path-mod-assign [data-region="review-panel"].collapsed + [data-region="grade-panel"] {
position: absolute;
- top: 7em;
- left: 0;
+ left: 30px;
right: 0;
- width: 99%;
+ width: calc(100% - 30px);
overflow: auto;
- bottom: 7em;
+ -webkit-transition: width 0.5s, right 0.5s, left 0.5s;
+ -moz-transition: width 0.5s, right 0.5s, left 0.5s;
+ transition: width 0.5s, right 0.5s, left 0.5s;
}
+.path-mod-assign [data-region="review-panel"].collapsed + [data-region="grade-panel"] [data-region="grade"],
.path-mod-assign [data-region="grade-panel"].fullwidth [data-region="grade"] {
max-width: 800px;
margin-left: auto;
top: 85px;
bottom: 60px;
right: 0;
+ left: 70%;
width: 30%;
overflow: auto;
box-sizing: border-box;
background-color: #f5f5f5;
padding: 15px;
padding-top: 0px;
+ -webkit-transition: width 0.5s, right 0.5s, left 0.5s;
+ -moz-transition: width 0.5s, right 0.5s, left 0.5s;
+ transition: width 0.5s, right 0.5s, left 0.5s;
+}
+
+.path-mod-assign [data-region="grade-panel"].collapsed {
+ left: calc(100% - 30px);
+ right: calc(30px - 100%);
+ visibility: hidden;
+ -webkit-transition: right 0.5s, left 0.5s, visibility 0s 0.5s;
+ -moz-transition: right 0.5s, left 0.5s, visibility 0s 0.5s;
+ transition: right 0.5s, left 0.5s, visibility 0s 0.5s;
+}
+
+.path-mod-assign [data-region="grade-panel"].fullwidth {
+ left: 0;
+ width: 100%;
+ overflow: auto;
}
.path-mod-assign [data-region="grade-panel"] h3 {
height: 60px;
}
+.path-mod-assign [data-region="grade-actions-panel"] [data-region="grade-actions"] .collapse-buttons {
+ position: absolute;
+ top: 0;
+ left: auto;
+ right: 15px;
+ margin: 0;
+ height: 100%;
+ line-height: 60px;
+ direction: ltr;
+}
+
.path-mod-assign [data-region="grade-actions"] {
padding: 1em;
text-align: center;
.path-mod-assign .page-context-header .page-header-headings {
margin-top: 13px;
}
+ .path-mod-assign [data-region="grade-actions-panel"] [data-region="grade-actions"] .collapse-buttons {
+ display: none;
+ }
.path-mod-assign [data-region="grading-navigation-panel"],
.path-mod-assign [data-region="review-panel"],
.path-mod-assign [data-region="grade-panel"],
+ .path-mod-assign [data-region="review-panel"].collapsed + [data-region="grade-panel"],
.path-mod-assign [data-region="grade-actions-panel"] {
position: inherit;
width: 100%;
top: 0;
left: 0;
+ right: auto;
overflow: auto;
height: auto;
margin-bottom: 1em;
}
+ .path-mod-assign [data-region="grade-panel"].collapsed {
+ visibility: visible;
+ }
.path-mod-assign [data-region="grading-navigation"] {
padding: 0;
text-align: center;
.path-mod-assign [data-region="grade-panel"] [data-region="popout-button"] {
display: none;
}
+ .path-mod-assign [data-region="review-panel"] {
+ position: relative;
+ max-height: 2000px;
+ -webkit-transition: max-height 0.25s linear;
+ -moz-transition: max-height 0.25s linear;
+ transition: max-height 0.25s linear;
+ }
.path-mod-assign [data-region="review-panel"] .pageheader {
border-right: none;
+ padding-right: 20px;
+ padding-left: 40px;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] {
+ display: block;
+ height: 20px;
+ width: 20px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: auto;
+ box-sizing: border-box;
+ border: 1px solid #cccccc;
+ border-radius: 0 0 4px 0;
+ background-color: #fff;
+ z-index: 99999;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon,
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon {
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ line-height: 30px;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .ltr-icon,
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .rtl-icon {
+ height: 100%;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .ltr-icon {
+ float: left;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .rtl-icon {
+ float: right;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .toggle-text,
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon .toggle-text {
+ line-height: 20px;
+ visibility: visible;
+ opacity: 1;
+ height: auto;
+ -webkit-transition: visibility 0s 0.25s, opacity 0s 0.25s;
+ -moz-transition: visibility 0s 0.25s, opacity 0s 0.25s;
+ transition: visibility 0s 0.25s, opacity 0s 0.25s;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .rtl-icon {
+ display: none;
+ }
+ .path-mod-assign [data-region="review-panel"].collapsed {
+ max-height: 20px;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ -webkit-transition: max-height 0.25s linear;
+ -moz-transition: max-height 0.25s linear;
+ transition: max-height 0.25s linear;
+ }
+ .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] {
+ width: 100%;
+ border-radius: 0 0 0 0;
+ -webkit-transition: all 0s 0.25s;
+ -moz-transition: all 0s 0.25s;
+ transition: all 0s 0.25s;
+ }
+ .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] img {
+ height: 100%;
+ }
+ .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon {
+ display: block;
+ }
+ .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .collapse-icon {
+ display: none;
}
.path-mod-assign.pagelayout-popup {
overflow: inherit;
float: none;
margin: 0 auto 10px;
}
+ .dir-rtl.path-mod-assign [data-region="review-panel"] .pageheader {
+ padding-right: 40px;
+ padding-left: 20px;
+ }
+ .dir-rtl.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] {
+ left: auto;
+ right: 0;
+ border-radius: 0 0 0 4px;
+ }
+ .dir-rtl.path-mod-assign [data-region="review-panel"].collapsed {
+ right: 0;
+ left: auto;
+ }
+ .dir-rtl.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .ltr-icon {
+ display: none;
+ }
+ .dir-rtl.path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .rtl-icon {
+ display: block;
+ }
+ .dir-rtl.path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .collapse-icon {
+ display: none;
+ }
+ .dir-rtl.path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .expand-icon {
+ display: block;
+ }
}
/** Start of CSS to make forms vertical in the grading panel (taken from theme/bootstrapbase/less/moodle/forms.less). */
return $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA) == 0;
}
+ /**
+ * Determine if a submission is empty
+ *
+ * This is distinct from is_empty in that it is intended to be used to
+ * determine if a submission made before saving is empty.
+ *
+ * @param stdClass $data The submission data
+ * @return bool
+ */
+ public function submission_is_empty(stdClass $data) {
+ $files = file_get_drafarea_files($data->files_filemanager);
+ return count($files->list) == 0;
+ }
+
/**
* Get file areas returns a list of areas this plugin stores files
* @return array - An array of fileareas (keys) and descriptions (values)
--- /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/>.
+
+/**
+ * Tests for mod/assign/submission/file/locallib.php
+ *
+ * @package assignsubmission_file
+ * @copyright 2016 Cameron Ball
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Unit tests for mod/assign/submission/file/locallib.php
+ *
+ * @copyright 2016 Cameron Ball
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignsubmission_file_locallib_testcase extends advanced_testcase {
+
+ /** @var stdClass $user A user to submit an assignment. */
+ protected $user;
+
+ /** @var stdClass $course New course created to hold the assignment activity. */
+ protected $course;
+
+ /** @var stdClass $cm A context module object. */
+ protected $cm;
+
+ /** @var stdClass $context Context of the assignment activity. */
+ protected $context;
+
+ /** @var stdClass $assign The assignment object. */
+ protected $assign;
+
+ /**
+ * Setup all the various parts of an assignment activity including creating an onlinetext submission.
+ */
+ protected function setUp() {
+ $this->user = $this->getDataGenerator()->create_user();
+ $this->course = $this->getDataGenerator()->create_course();
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $params = [
+ 'course' => $this->course->id,
+ 'assignsubmission_file_enabled' => 1,
+ 'assignsubmission_file_maxfiles' => 12,
+ 'assignsubmission_file_maxsizebytes' => 10,
+ ];
+ $instance = $generator->create_instance($params);
+ $this->cm = get_coursemodule_from_instance('assign', $instance->id);
+ $this->context = context_module::instance($this->cm->id);
+ $this->assign = new testable_assign($this->context, $this->cm, $this->course);
+ $this->setUser($this->user->id);
+ }
+
+ /**
+ * Test submission_is_empty
+ *
+ * @dataProvider submission_is_empty_testcases
+ * @param string $data The file submission data
+ * @param bool $expected The expected return value
+ */
+ public function test_submission_is_empty($data, $expected) {
+ $this->resetAfterTest();
+
+ $itemid = file_get_unused_draft_itemid();
+ $submission = (object)['files_filemanager' => $itemid];
+ $plugin = $this->assign->get_submission_plugin_by_type('file');
+
+ if ($data) {
+ $data += ['contextid' => context_user::instance($this->user->id)->id, 'itemid' => $itemid];
+ $fs = get_file_storage();
+ $fs->create_file_from_string((object)$data, 'Content of ' . $data['filename']);
+ }
+
+ $result = $plugin->submission_is_empty($submission);
+ $this->assertTrue($result === $expected);
+ }
+
+ /**
+ * Test new_submission_empty
+ *
+ * @dataProvider submission_is_empty_testcases
+ * @param string $data The file submission data
+ * @param bool $expected The expected return value
+ */
+ public function test_new_submission_empty($data, $expected) {
+ $this->resetAfterTest();
+
+ $itemid = file_get_unused_draft_itemid();
+ $submission = (object)['files_filemanager' => $itemid];
+
+ if ($data) {
+ $data += ['contextid' => context_user::instance($this->user->id)->id, 'itemid' => $itemid];
+ $fs = get_file_storage();
+ $fs->create_file_from_string((object)$data, 'Content of ' . $data['filename']);
+ }
+
+ $result = $this->assign->new_submission_empty($submission);
+ $this->assertTrue($result === $expected);
+ }
+
+ /**
+ * Dataprovider for the test_submission_is_empty testcase
+ *
+ * @return array of testcases
+ */
+ public function submission_is_empty_testcases() {
+ return [
+ 'With file' => [
+ [
+ 'component' => 'user',
+ 'filearea' => 'draft',
+ 'filepath' => '/',
+ 'filename' => 'not_a_virus.exe'
+ ],
+ false
+ ],
+ 'Without file' => [null, true]
+ ];
+ }
+
+
+}
return empty($onlinetextsubmission->onlinetext);
}
+ /**
+ * Determine if a submission is empty
+ *
+ * This is distinct from is_empty in that it is intended to be used to
+ * determine if a submission made before saving is empty.
+ *
+ * @param stdClass $data The submission data
+ * @return bool
+ */
+ public function submission_is_empty(stdClass $data) {
+ if (!isset($data->onlinetext_editor)) {
+ return true;
+ }
+ return !strlen((string)$data->onlinetext_editor['text']);
+ }
+
/**
* Get file areas returns a list of areas this plugin stores files
* @return array - An array of fileareas (keys) and descriptions (values)
* @return external_description|null
*/
public function get_external_parameters() {
- $editorparams = array('text' => new external_value(PARAM_TEXT, 'The text for this submission.'),
+ $editorparams = array('text' => new external_value(PARAM_RAW, 'The text for this submission.'),
'format' => new external_value(PARAM_INT, 'The format for this submission'),
'itemid' => new external_value(PARAM_INT, 'The draft area id for files attached to the submission'));
$editorstructure = new external_single_structure($editorparams, 'Editor structure', VALUE_OPTIONAL);
--- /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/>.
+
+/**
+ * Tests for mod/assign/submission/onlinetext/locallib.php
+ *
+ * @package assignsubmission_onlinetext
+ * @copyright 2016 Cameron Ball
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Unit tests for mod/assign/submission/onlinetext/locallib.php
+ *
+ * @copyright 2016 Cameron Ball
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignsubmission_onlinetext_locallib_testcase extends advanced_testcase {
+
+ /** @var stdClass $user A user to submit an assignment. */
+ protected $user;
+
+ /** @var stdClass $course New course created to hold the assignment activity. */
+ protected $course;
+
+ /** @var stdClass $cm A context module object. */
+ protected $cm;
+
+ /** @var stdClass $context Context of the assignment activity. */
+ protected $context;
+
+ /** @var stdClass $assign The assignment object. */
+ protected $assign;
+
+ /**
+ * Setup all the various parts of an assignment activity including creating an onlinetext submission.
+ */
+ protected function setUp() {
+ $this->user = $this->getDataGenerator()->create_user();
+ $this->course = $this->getDataGenerator()->create_course();
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $params = ['course' => $this->course->id, 'assignsubmission_onlinetext_enabled' => 1];
+ $instance = $generator->create_instance($params);
+ $this->cm = get_coursemodule_from_instance('assign', $instance->id);
+ $this->context = context_module::instance($this->cm->id);
+ $this->assign = new testable_assign($this->context, $this->cm, $this->course);
+ $this->setUser($this->user->id);
+ }
+
+ /**
+ * Test submission_is_empty
+ *
+ * @dataProvider submission_is_empty_testcases
+ * @param string $submissiontext The online text submission text
+ * @param bool $expected The expected return value
+ */
+ public function test_submission_is_empty($submissiontext, $expected) {
+ $this->resetAfterTest();
+
+ $plugin = $this->assign->get_submission_plugin_by_type('onlinetext');
+ $data = new stdClass();
+ $data->onlinetext_editor = ['text' => $submissiontext];
+
+ $result = $plugin->submission_is_empty($data);
+ $this->assertTrue($result === $expected);
+ }
+
+ /**
+ * Test new_submission_empty
+ *
+ * @dataProvider submission_is_empty_testcases
+ * @param string $submissiontext The file submission data
+ * @param bool $expected The expected return value
+ */
+ public function test_new_submission_empty($submissiontext, $expected) {
+ $this->resetAfterTest();
+ $data = new stdClass();
+ $data->onlinetext_editor = ['text' => $submissiontext];
+
+ $result = $this->assign->new_submission_empty($data);
+ $this->assertTrue($result === $expected);
+ }
+
+ /**
+ * Dataprovider for the test_submission_is_empty testcase
+ *
+ * @return array of testcases
+ */
+ public function submission_is_empty_testcases() {
+ return [
+ 'Empty submission string' => ['', true],
+ 'Empty submission null' => [null, true],
+ 'Value 0' => [0, false],
+ 'String 0' => ['0', false],
+ 'Text' => ['Ai! laurië lantar lassi súrinen, yéni únótimë ve rámar aldaron!', false]
+ ];
+ }
+}
* Define this form - called by the parent constructor
*/
public function definition() {
+ global $USER;
$mform = $this->_form;
-
list($assign, $data) = $this->_customdata;
- $assign->add_submission_form_elements($mform, $data);
+ $instance = $assign->get_instance();
+ if ($instance->teamsubmission) {
+ $submission = $assign->get_group_submission($USER->id, 0, true);
+ } else {
+ $submission = $assign->get_user_submission($USER->id, true);
+ }
+ if ($submission) {
+ $mform->addElement('hidden', 'lastmodified', $submission->timemodified);
+ $mform->setType('lastmodified', PARAM_INT);
+ }
+ $assign->add_submission_form_elements($mform, $data);
$this->add_action_buttons(true, get_string('savechanges', 'assign'));
if ($data) {
$this->set_data($data);
public function add_attempt(stdClass $oldsubmission, stdClass $newsubmission) {
}
+ /**
+ * Determine if a submission is empty
+ *
+ * This is distinct from is_empty in that it is intended to be used to
+ * determine if a submission made before saving is empty.
+ *
+ * @param stdClass $data The submission data
+ * @return bool
+ */
+ public function submission_is_empty(stdClass $data) {
+ return false;
+ }
}
<button type="submit" class="btn btn-primary" name="savechanges">{{#str}}savechanges{{/str}}</button>
<button type="submit" class="btn" name="resetbutton">{{#str}}reset{{/str}}</button>
</form>
+{{#showreview}}
+<div class="btn-toolbar collapse-buttons">
+ <div class="btn-group">
+ <button class="btn collapse-review-panel">{{#pix}} layout-expand-right, mod_assign, {{#str}} collapsereviewpanel, mod_assign {{/str}} {{/pix}}</button>
+ <button class="btn collapse-none active">{{#pix}} layout-default, mod_assign, {{#str}} defaultlayout, mod_assign {{/str}} {{/pix}}</button>
+ <button class="btn collapse-grade-panel">{{#pix}} layout-expand-left, mod_assign, {{#str}} collapsegradepanel, mod_assign {{/str}} {{/pix}}</button>
+ </div>
+</div>
+{{/showreview}}
{{#js}}
require(['mod_assign/grading_actions'], function(GradingActions) {
new GradingActions('[data-region="grade-actions"]');
<div data-region="grading-navigation-panel" data-first-userid="{{userid}}" data-courseid="{{courseid}}" data-showuseridentity="{{showuseridentity}}">
{{> mod_assign/grading_navigation }}
</div>
+{{#showreview}}
+<div data-region="review-panel">
+ <a href="#" data-region="review-panel-toggle">
+ <div class="collapse-icon">{{#pix}} t/expanded, core, {{#str}} collapsereviewpanel, mod_assign {{/str}} {{/pix}}</div>
+ <div class="expand-icon">
+ <div class="ltr-icon">{{#pix}} t/collapsed, core, {{#str}} expandreviewpanel, mod_assign {{/str}} {{/pix}}</div>
+ <div class="rtl-icon">{{#pix}} t/collapsed_rtl, core, {{#str}} expandreviewpanel, mod_assign {{/str}} {{/pix}}</div>
+ <div class="toggle-text">{{#str}} clickexpandreviewpanel, mod_assign {{/str}}</div>
+ </div>
+ </a>
+ <div data-region="review-panel-content">
+ <div data-region="review">
+ {{> mod_assign/review_panel }}
+ </div>
+ </div>
+</div>
+{{/showreview}}
<div data-region="grade-panel" {{^showreview}}class="fullwidth"{{/showreview}}>
<div data-region="grade" data-contextid="{{contextid}}" data-assignmentid="{{assignmentid}}">
{{> mod_assign/grading_panel }}
</div>
</div>
-{{#showreview}}
-<div data-region="review-panel">
-<div data-region="review">
-{{> mod_assign/review_panel }}
-</div>
-</div>
-{{/showreview}}
<div data-region="grade-actions-panel">
<div data-region="grade-actions">
{{> mod_assign/grading_actions }}
// Create a course.
$course1 = self::getDataGenerator()->create_course(array(
'idnumber' => 'idnumbercourse1',
- 'fullname' => 'Lightwork Course 1',
+ 'fullname' => '<b>Lightwork Course 1</b>', // Adding tags here to check that external_format_string works.
+ 'shortname' => '<b>Lightwork Course 1</b>', // Adding tags here to check that external_format_string works.
'summary' => 'Lightwork Course 1 description',
'summaryformat' => FORMAT_MOODLE,
'category' => $category->id
$this->assertEquals(1, count($result['courses']));
$course = $result['courses'][0];
$this->assertEquals('Lightwork Course 1', $course['fullname']);
+ $this->assertEquals('Lightwork Course 1', $course['shortname']);
$this->assertEquals(1, count($course['assignments']));
$assignment = $course['assignments'][0];
$this->assertEquals($assign1->id, $assignment['id']);
// Now try a submission.
$submissionpluginparams = array();
$submissionpluginparams['files_filemanager'] = $draftidfile;
- $onlinetexteditorparams = array('text'=>'Yeeha!',
+ $onlinetexteditorparams = array('text' => '<p>Yeeha!</p>',
'format'=>1,
'itemid'=>$draftidonlinetext);
$submissionpluginparams['onlinetext_editor'] = $onlinetexteditorparams;
$this->assertContains(get_string('submitassignment', 'assign'), $output, 'Can submit non empty onlinetext assignment');
}
+ /**
+ * Test new_submission_empty
+ *
+ * We only test combinations of plugins here. Individual plugins are tested
+ * in their respective test files.
+ *
+ * @dataProvider test_new_submission_empty_testcases
+ * @param string $data The file submission data
+ * @param bool $expected The expected return value
+ */
+ public function test_new_submission_empty($data, $expected) {
+ $this->resetAfterTest();
+ $assign = $this->create_instance(['assignsubmission_file_enabled' => 1,
+ 'assignsubmission_file_maxfiles' => 12,
+ 'assignsubmission_file_maxsizebytes' => 10,
+ 'assignsubmission_onlinetext_enabled' => 1]);
+ $this->setUser($this->students[0]);
+ $submission = new stdClass();
+
+ if ($data['file'] && isset($data['file']['filename'])) {
+ $itemid = file_get_unused_draft_itemid();
+ $submission->files_filemanager = $itemid;
+ $data['file'] += ['contextid' => context_user::instance($this->students[0]->id)->id, 'itemid' => $itemid];
+ $fs = get_file_storage();
+ $fs->create_file_from_string((object)$data['file'], 'Content of ' . $data['file']['filename']);
+ }
+
+ if ($data['onlinetext']) {
+ $submission->onlinetext_editor = ['text' => $data['onlinetext']];
+ }
+
+ $result = $assign->new_submission_empty($submission);
+ $this->assertTrue($result === $expected);
+ }
+
+ /**
+ * Dataprovider for the test_new_submission_empty testcase
+ *
+ * @return array of testcases
+ */
+ public function test_new_submission_empty_testcases() {
+ return [
+ 'With file and onlinetext' => [
+ [
+ 'file' => [
+ 'component' => 'user',
+ 'filearea' => 'draft',
+ 'filepath' => '/',
+ 'filename' => 'not_a_virus.exe'
+ ],
+ 'onlinetext' => 'Balin Fundinul Uzbadkhazaddumu'
+ ],
+ false
+ ]
+ ];
+ }
+
public function test_list_participants() {
global $CFG, $DB;
=== 3.2 ===
* External function mod_assign_external::get_assignments now returns additional optional fields:
- preventsubmissionnotingroup: Prevent submission not in group.
+* Proper checking for empty submissions
+* Submission modification time checking - this will help students working in groups not clobber each others'
+ submissions
=== 3.1 ===
* The feedback plugins now need to implement the is_feedback_modified() method. The default is to return true
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version = 2016052300; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2016070400; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2016051900; // Requires this Moodle version.
$plugin->cron = 60;
// Format intro.
list($bookdetails['intro'], $bookdetails['introformat']) =
external_format_text($book->intro, $book->introformat, $context->id, 'mod_book', 'intro', null);
+ $bookdetails['introfiles'] = external_util::get_area_files($context->id, 'mod_book', 'intro', false, false);
$bookdetails['numbering'] = $book->numbering;
$bookdetails['navstyle'] = $book->navstyle;
$bookdetails['customtitles'] = $book->customtitles;
'name' => new external_value(PARAM_RAW, 'Book name'),
'intro' => new external_value(PARAM_RAW, 'The Book intro'),
'introformat' => new external_format_value('intro'),
+ 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
'numbering' => new external_value(PARAM_INT, 'Book numbering configuration'),
'navstyle' => new external_value(PARAM_INT, 'Book navigation style configuration'),
'customtitles' => new external_value(PARAM_INT, 'Book custom titles type'),
$books = external_api::clean_returnvalue(mod_book_external::get_books_by_courses_returns(), $books);
$this->assertCount(1, $books['books']);
$this->assertEquals('First Book', $books['books'][0]['name']);
- // We see 9 fields.
- $this->assertCount(9, $books['books'][0]);
+ // We see 10 fields.
+ $this->assertCount(10, $books['books'][0]);
// As Student you cannot see some book properties like 'section'.
$this->assertFalse(isset($books['books'][0]['section']));
$this->assertCount(1, $books['books']);
$this->assertEquals('Second Book', $books['books'][0]['name']);
- // We see 16 fields.
- $this->assertCount(16, $books['books'][0]);
+ // We see 17 fields.
+ $this->assertCount(17, $books['books'][0]);
// As an Admin you can see some book properties like 'section'.
$this->assertEquals(0, $books['books'][0]['section']);
// Format intro.
list($chatdetails['intro'], $chatdetails['introformat']) =
external_format_text($chat->intro, $chat->introformat, $chatcontext->id, 'mod_chat', 'intro', null);
+ $chatdetails['introfiles'] = external_util::get_area_files($chatcontext->id, 'mod_chat', 'intro', false, false);
if (has_capability('mod/chat:chat', $chatcontext)) {
$chatdetails['chatmethod'] = $CFG->chat_method;
'name' => new external_value(PARAM_RAW, 'Chat name'),
'intro' => new external_value(PARAM_RAW, 'The Chat intro'),
'introformat' => new external_format_value('intro'),
+ 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
'chatmethod' => new external_value(PARAM_ALPHA, 'chat method (sockets, daemon)', VALUE_OPTIONAL),
'keepdays' => new external_value(PARAM_INT, 'keep days', VALUE_OPTIONAL),
'studentlogs' => new external_value(PARAM_INT, 'student logs visible to everyone', VALUE_OPTIONAL),
header('Content-Type: text/html; charset=utf-8');
header("Refresh: $CFG->chat_refresh_room; url=$refreshurl");
-// Required stylesheets.
-$stylesheetshtml = '';
-/*foreach ($CFG->stylesheets as $stylesheet) {
- //TODO: MDL-21120
- $stylesheetshtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />';
-}*/
-
// Use ob to be able to send Content-Length headers.
// Needed for Keep-Alive to work.
ob_start();
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script type="text/javascript">
//<![CDATA[
- function safari_refresh() {
- self.location.href= '<?php echo $refreshurl;?>';
- }
- var issafari = false;
- if (window.devicePixelRatio) {
- issafari = true;
- setTimeout('safari_refresh()', <?php echo $CFG->chat_refresh_room * 1000;?>);
- }
if (parent.msg && parent.msg.document.getElementById("msgStarted") == null) {
parent.msg.document.close();
parent.msg.document.open("text/html","replace");
parent.msg.document.write("<html><head>");
parent.msg.document.write("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />");
parent.msg.document.write("<base target=\"_blank\" />");
- parent.msg.document.write("<?php echo addslashes_js($stylesheetshtml) ?>");
parent.msg.document.write("<\/head><body class=\"mod-chat-gui_header_js course-<?php echo $chatuser->course ?>\" id=\"mod-chat-gui_header_js-jsupdate\"><div style=\"display: none\" id=\"msgStarted\"> <\/div>");
}
<?php
header('Pragma: no-cache');
header('Content-Type: text/html; charset=utf-8');
-// Required stylesheets.
-$stylesheetshtml = '';
-/*foreach ($CFG->stylesheets as $stylesheet) {
- //TODO: MDL-21120
- $stylesheetshtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />';
-}*/
-
$refreshurl = "{$CFG->wwwroot}/mod/chat/gui_header_js/jsupdated.php?".
"chat_sid=$chatsid&chat_lasttime=$chatlasttime&chat_lastrow=$chatnewrow&chat_lastid=$chatlastid";
?>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script type="text/javascript">
//<![CDATA[
- function safari_refresh() {
- self.location.href= '<?php echo $refreshurl;?>';
- }
- var issafari = false;
- if(window.devicePixelRatio){
- issafari = true;
- setTimeout('safari_refresh()', <?php echo $CFG->chat_refresh_room * 1000;?>);
- }
if (parent.msg.document.getElementById("msgStarted") == null) {
parent.msg.document.close();
parent.msg.document.open("text/html","replace");
parent.msg.document.write("<html><head>");
parent.msg.document.write("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />");
parent.msg.document.write("<base target=\"_blank\" />");
- parent.msg.document.write("<?php echo addslashes_js($stylesheetshtml) ?>");
parent.msg.document.write("</head><body class=\"mod-chat-gui_header_js course-<?php echo $chatuser->course ?>\" id=\"mod-chat-gui_header_js-jsupdate\"><div style=\"display: none\" id=\"msgStarted\"> </div>");
}
//]]>
$chats = external_api::clean_returnvalue(mod_chat_external::get_chats_by_courses_returns(), $chats);
$this->assertCount(1, $chats['chats']);
$this->assertEquals('First Chat', $chats['chats'][0]['name']);
- // We see 11 fields.
- $this->assertCount(11, $chats['chats'][0]);
+ // We see 12 fields.
+ $this->assertCount(12, $chats['chats'][0]);
// As Student you cannot see some chat properties like 'section'.
$this->assertFalse(isset($chats['chats'][0]['section']));
$this->assertCount(1, $chats['chats']);
$this->assertEquals('Second Chat', $chats['chats'][0]['name']);
- // We see 16 fields.
- $this->assertCount(16, $chats['chats'][0]);
+ // We see 17 fields.
+ $this->assertCount(17, $chats['chats'][0]);
// As an Admin you can see some chat properties like 'section'.
$this->assertEquals(0, $chats['chats'][0]['section']);
list($choicedetails['intro'], $choicedetails['introformat']) =
external_format_text($choice->intro, $choice->introformat,
$context->id, 'mod_choice', 'intro', null);
+ $choicedetails['introfiles'] = external_util::get_area_files($context->id, 'mod_choice', 'intro', false,
+ false);
if (has_capability('mod/choice:choose', $context)) {
$choicedetails['publish'] = $choice->publish;
'name' => new external_value(PARAM_RAW, 'Choice name'),
'intro' => new external_value(PARAM_RAW, 'The choice intro'),
'introformat' => new external_format_value('intro'),
+ 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
'publish' => new external_value(PARAM_BOOL, 'If choice is published', VALUE_OPTIONAL),
'showresults' => new external_value(PARAM_INT, '0 never, 1 after answer, 2 after close, 3 always',
VALUE_OPTIONAL),
* @param object $choices
* @return string
*/
- public function display_publish_anonymous_vertical($choices) {
+ public function display_publish_anonymous_horizontal($choices) {
global $CHOICE_COLUMN_HEIGHT;
$html = '';
* @param object $choices
* @return string
*/
- public function display_publish_anonymous_horizontal($choices) {
+ public function display_publish_anonymous_vertical($choices) {
global $CHOICE_COLUMN_WIDTH;
$table = new html_table();
list($newdb['intro'], $newdb['introformat']) =
external_format_text($database->intro, $database->introformat,
$datacontext->id, 'mod_data', 'intro', null);
+ $newdb['introfiles'] = external_util::get_area_files($datacontext->id, 'mod_data', 'intro', false, false);
// This information should be only available if the user can see the database entries.
if (has_capability('mod/data:viewentry', $datacontext)) {
'name' => new external_value(PARAM_RAW, 'Database name'),
'intro' => new external_value(PARAM_RAW, 'The Database intro'),
'introformat' => new external_format_value('intro'),
+ 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
'comments' => new external_value(PARAM_BOOL, 'comments enabled', VALUE_OPTIONAL),
'timeavailablefrom' => new external_value(PARAM_INT, 'timeavailablefrom field', VALUE_OPTIONAL),
'timeavailableto' => new external_value(PARAM_INT, 'timeavailableto field', VALUE_OPTIONAL),
// First for the student user.
$expectedfields = array('id', 'coursemodule', 'course', 'name', 'comments', 'timeavailablefrom',
'timeavailableto', 'timeviewfrom', 'timeviewto', 'requiredentries', 'requiredentriestoview',
- 'intro', 'introformat');
+ 'intro', 'introformat', 'introfiles');
// Add expected coursemodule.
$database1->coursemodule = $database1->cmid;
+ $database1->introfiles = [];
$database2->coursemodule = $database2->cmid;
+ $database2->introfiles = [];
$expected1 = array();
$expected2 = array();
// Format the intro before being returning using the format setting.
list($forum->intro, $forum->introformat) = external_format_text($forum->intro, $forum->introformat,
$context->id, 'mod_forum', 'intro', 0);
+ $forum->introfiles = external_util::get_area_files($context->id, 'mod_forum', 'intro', false, false);
// Discussions count. This function does static request cache.
$forum->numdiscussions = forum_count_discussions($forum, $cm, $course);
$forum->cmid = $forum->coursemodule;
'name' => new external_value(PARAM_RAW, 'Forum name'),
'intro' => new external_value(PARAM_RAW, 'The forum intro'),
'introformat' => new external_format_value('intro'),
+ 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
'assessed' => new external_value(PARAM_INT, 'Aggregate type'),
'assesstimestart' => new external_value(PARAM_INT, 'Assess start time'),
'assesstimefinish' => new external_value(PARAM_INT, 'Assess finish time'),
$record->introformat = FORMAT_HTML;
$record->course = $course2->id;
$forum2 = self::getDataGenerator()->create_module('forum', $record);
+ $forum2->introfiles = [];
// Add discussions to the forums.
$record = new stdClass();
// Expect one discussion.
$forum1->numdiscussions = 1;
$forum1->cancreatediscussions = true;
+ $forum1->introfiles = [];
$record = new stdClass();
$record->course = $course2->id;
$glossary->name = external_format_string($glossary->name, $context->id);
list($glossary->intro, $glossary->introformat) = external_format_text($glossary->intro, $glossary->introformat,
$context->id, 'mod_glossary', 'intro', null);
+ $glossary->introfiles = external_util::get_area_files($context->id, 'mod_glossary', 'intro', false, false);
// Make sure we have a number of entries per page.
if (!$glossary->entbypage) {
'name' => new external_value(PARAM_RAW, 'Glossary name'),
'intro' => new external_value(PARAM_RAW, 'The Glossary intro'),
'introformat' => new external_format_value('intro'),
+