+// 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));
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);
}
{
+ "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();
* @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,
@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);
+ }
+ }
+ }
}
/** @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);
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);
}
/**
* 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() {
'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(
$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}.';
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);
}
+
+ // 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 {
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)
--- /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;
+ }
}
$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;
<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");
<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");
* @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();
return new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'Tool type id'),
- 'name' => new external_value(PARAM_TEXT, 'Tool type name'),
- 'description' => new external_value(PARAM_TEXT, 'Tool type description'),
+ 'name' => new external_value(PARAM_NOTAGS, 'Tool type name'),
+ 'description' => new external_value(PARAM_NOTAGS, 'Tool type description'),
'urls' => new external_single_structure(
array(
'icon' => new external_value(PARAM_URL, 'Tool type icon URL'),
return new external_function_parameters(
array(
'id' => new external_value(PARAM_INT, 'Tool type id'),
- 'name' => new external_value(PARAM_TEXT, 'Tool type name', VALUE_DEFAULT, null),
- 'description' => new external_value(PARAM_TEXT, 'Tool type description', VALUE_DEFAULT, null),
+ 'name' => new external_value(PARAM_RAW, 'Tool type name', VALUE_DEFAULT, null),
+ 'description' => new external_value(PARAM_RAW, 'Tool type description', VALUE_DEFAULT, null),
'state' => new external_value(PARAM_INT, 'Tool type state', VALUE_DEFAULT, null)
)
);
$type = new stdClass();
$type->modclass = MOD_CLASS_ACTIVITY;
$type->name = 'lti_type_' . $ltitype->id;
- $type->title = $ltitype->name;
+ // Clean the name. We don't want tags here.
+ $type->title = clean_param($ltitype->name, PARAM_NOTAGS);
$trimmeddescription = trim($ltitype->description);
if ($trimmeddescription != '') {
- $type->help = $trimmeddescription;
+ // Clean the description. We don't want tags here.
+ $type->help = clean_param($trimmeddescription, PARAM_NOTAGS);
$type->helplink = get_string('modulename_shortcut_link', 'lti');
}
if (empty($ltitype->icon)) {
function serialise_tool_type(stdClass $type) {
$capabilitygroups = get_tool_type_capability_groups($type);
$instanceids = get_tool_type_instance_ids($type);
-
+ // Clean the name. We don't want tags here.
+ $name = clean_param($type->name, PARAM_NOTAGS);
+ if (!empty($type->description)) {
+ // Clean the description. We don't want tags here.
+ $description = clean_param($type->description, PARAM_NOTAGS);
+ } else {
+ $description = get_string('editdescription', 'mod_lti');
+ }
return array(
'id' => $type->id,
- 'name' => $type->name,
- 'description' => isset($type->description) ? $type->description : get_string('editdescription', 'mod_lti'),
+ 'name' => $name,
+ 'description' => $description,
'urls' => get_tool_type_urls($type),
'state' => get_tool_type_state_info($type),
'hascapabilitygroups' => !empty($capabilitygroups),
$scoes->elements[$manifest][$organization][$identifier]->launch = '';
$scoes->elements[$manifest][$organization][$identifier]->scormtype = 'asset';
} else {
- $idref = $block['attrs']['IDENTIFIERREF'];
+ $idref = addslashes_js($block['attrs']['IDENTIFIERREF']);
$base = '';
if (isset($resources[$idref]['XML:BASE'])) {
$base = $resources[$idref]['XML:BASE'];
$string['calculatedweight'] = 'Calculated weight';
$string['cannotaccess'] = 'You cannot call this script in that way';
$string['cannotfindsco'] = 'Could not find SCO';
+$string['closebeforeopen'] = 'You have specified a close date before the open date.';
$string['collapsetocwinsize'] = 'Collapse TOC when window size below';
$string['collapsetocwinsizedesc'] = 'This setting lets you specify the window size below which the TOC should automatically collapse.';
$string['compatibilitysettings'] = 'Compatibility settings';
}
+ // Validate availability dates.
+ if ($data['timeopen'] && $data['timeclose']) {
+ if ($data['timeopen'] > $data['timeclose']) {
+ $errors['timeclose'] = get_string('closebeforeopen', 'scorm');
+ }
+ }
+
return $errors;
}
// Define each element separated
$survey = new backup_nested_element('survey', array('id'), array(
'name', 'intro', 'introformat', 'template',
- 'questions', 'days', 'timecreated', 'timemodified'));
+ 'questions', 'days', 'timecreated', 'timemodified', 'completionsubmit'));
$answers = new backup_nested_element('answers');
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/survey/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/survey"
+<XMLDB PATH="mod/survey/db" VERSION="20160615" COMMENT="XMLDB file for Moodle mod/survey"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="intro text field format"/>
<FIELD NAME="questions" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="completionsubmit" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If this field is set to 1, then the activity will be automatically marked as 'complete' once the user submits the survey."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
defined('MOODLE_INTERNAL') || die();
function xmldb_survey_upgrade($oldversion) {
- global $CFG;
-
+ global $DB;
+ $dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes.
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v3.1.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2016061400) {
+
+ // Define field completionsubmit to be added to survey.
+ $table = new xmldb_table('survey');
+ $field = new xmldb_field('completionsubmit', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'questions');
+
+ // Conditionally launch add field completionsubmit.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Survey savepoint reached.
+ upgrade_mod_savepoint(true, 2016061400, 'survey');
+ }
return true;
}
$string['cannotfindanswer'] = 'There are no answers for this survey yet.';
$string['cannotfindquestion'] = 'Question doesn\'t exist';
$string['cannotfindsurveytmpt'] = 'No survey templates found!';
+$string['completionsubmit'] = 'Student must submit to this activity to complete it';
$string['ciqintro'] = 'While thinking about recent events in this class, answer the questions below.';
$string['ciqname'] = 'Critical incidents';
$string['ciq1'] = 'At what moment in class were you most engaged as a learner?';
case FEATURE_GROUPINGS: return true;
case FEATURE_MOD_INTRO: return true;
case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
+ case FEATURE_COMPLETION_HAS_RULES: return true;
case FEATURE_GRADE_HAS_GRADE: return false;
case FEATURE_GRADE_OUTCOMES: return false;
case FEATURE_BACKUP_MOODLE2: return true;
$DB->insert_records("survey_answers", $answerstoinsert);
}
+ // Update completion state.
+ $cm = get_coursemodule_from_instance('survey', $survey->id, $course->id);
+ $completion = new completion_info($course);
+ if (isloggedin() && !isguestuser() && $completion->is_enabled($cm) && $survey->completionsubmit) {
+ $completion->update_state($cm, COMPLETION_COMPLETE);
+ }
+
$params = array(
'context' => $context,
'courseid' => $course->id,
$event = \mod_survey\event\response_submitted::create($params);
$event->trigger();
}
+
+/**
+ * Obtains the automatic completion state for this survey based on the condition
+ * in feedback settings.
+ *
+ * @param object $course Course
+ * @param object $cm Course-module
+ * @param int $userid User ID
+ * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
+ * @return bool True if completed, false if not, $type if conditions not set.
+ */
+function survey_get_completion_state($course, $cm, $userid, $type) {
+ global $DB;
+
+ // Get survey details.
+ $survey = $DB->get_record('survey', array('id' => $cm->instance), '*', MUST_EXIST);
+
+ // If completion option is enabled, evaluate it and return true/false.
+ if ($survey->completionsubmit) {
+ $params = array('userid' => $userid, 'survey' => $survey->id);
+ return $DB->record_exists('survey_answers', $params);
+ } else {
+ // Completion option is not enabled so just return $type.
+ return $type;
+ }
+}
$this->add_action_buttons();
}
+ /**
+ * Return submitted data if properly submitted or returns NULL if validation fails or
+ * if there is no submitted data.
+ *
+ * @return stdClass submitted data; NULL if not valid or not submitted or cancelled
+ */
+ public function get_data() {
+ $data = parent::get_data();
+ if (!$data) {
+ return false;
+ }
+
+ if (!empty($data->completionunlocked)) {
+ // Turn off completion settings if the checkboxes aren't ticked.
+ $autocompletion = !empty($data->completion) &&
+ $data->completion == COMPLETION_TRACKING_AUTOMATIC;
+ if (!$autocompletion || empty($data->completionsubmit)) {
+ $data->completionsubmit = 0;
+ }
+ }
+ return $data;
+ }
+ /**
+ * Add completion rules to form.
+ * @return array
+ */
+ public function add_completion_rules() {
+ $mform =& $this->_form;
+ $mform->addElement('checkbox', 'completionsubmit', '', get_string('completionsubmit', 'survey'));
+ return array('completionsubmit');
+ }
+
+ /**
+ * Enable completion rules
+ * @param stdclass $data
+ * @return array
+ */
+ public function completion_rule_enabled($data) {
+ return !empty($data['completionsubmit']);
+ }
}
--- /dev/null
+@mod @mod_survey
+Feature: A teacher can use activity completion to track a student progress
+ In order to use activity completion
+ As a teacher
+ I need to set survey activities and enable activity completion
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category | enablecompletion |
+ | Course 1 | C1 | 0 | 1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+
+ Scenario: Require survey view
+ Given I add a "Survey" to section "1" and I fill the form with:
+ | Name | Test survey name |
+ | Survey type | Critical incidents |
+ | Description | Test survey description |
+ | Completion tracking | Show activity as complete when conditions are met |
+ | id_completionview | 1 |
+ And I turn editing mode off
+ And the "Test survey name" "survey" activity with "auto" completion should be marked as not complete
+ When I follow "Test survey name"
+ And I follow "Course 1"
+ Then the "Test survey name" "survey" activity with "auto" completion should be marked as complete
+
+ Scenario: Require survey submission
+ Given I add a "Survey" to section "1" and I fill the form with:
+ | Name | Test survey name |
+ | Survey type | Critical incidents |
+ | Description | Test survey description |
+ | Completion tracking | Show activity as complete when conditions are met |
+ | id_completionsubmit | 1 |
+ And I turn editing mode off
+ And the "Test survey name" "survey" activity with "auto" completion should be marked as not complete
+ When I follow "Test survey name"
+ And I press "Click here to continue"
+ And I follow "Course 1"
+ Then the "Test survey name" "survey" activity with "auto" completion should be marked as complete
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2016052300; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2016061400; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2016051900; // Requires this Moodle version
$plugin->component = 'mod_survey'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
// This may take a while. Raise the execution time limit.
core_php_time_limit::raise();
- // Find all the user pages.
- $where = 'userid IS NOT NULL AND private = :private';
- $params = array('private' => $private);
- $pages = $DB->get_recordset_select('my_pages', $where, $params, 'id, userid');
- $blockids = array();
-
- foreach ($pages as $page) {
- $usercontext = context_user::instance($page->userid);
-
- // Find all block instances in that page.
- $blockswhere = 'parentcontextid = :parentcontextid AND
- pagetypepattern = :pagetypepattern AND
- (subpagepattern IS NULL OR subpagepattern = :subpagepattern)';
- $blockswhereparams = [
- 'parentcontextid' => $usercontext->id,
- 'pagetypepattern' => $pagetype,
- 'subpagepattern' => $page->id
- ];
- if ($pageblockids = $DB->get_fieldset_select('block_instances', 'id', $blockswhere, $blockswhereparams)) {
- $blockids = array_merge($blockids, $pageblockids);
- }
- }
- $pages->close();
+ // Find all the user pages and all block instances in them.
+ $sql = "SELECT bi.id
+ FROM {my_pages} p
+ JOIN {context} ctx ON ctx.instanceid = p.userid AND ctx.contextlevel = :usercontextlevel
+ JOIN {block_instances} bi ON bi.parentcontextid = ctx.id AND
+ bi.pagetypepattern = :pagetypepattern AND
+ (bi.subpagepattern IS NULL OR bi.subpagepattern = " . $DB->sql_concat("''", 'p.id') . ")
+ WHERE p.private = :private";
+ $params = array('private' => $private,
+ 'usercontextlevel' => CONTEXT_USER,
+ 'pagetypepattern' => $pagetype);
+ $blockids = $DB->get_fieldset_sql($sql, $params);
// Wrap the SQL queries in a transaction.
$transaction = $DB->start_delegated_transaction();
}
// Finally delete the pages.
- if (!empty($pages)) {
- $DB->delete_records_select('my_pages', $where, $params);
- }
+ $DB->delete_records_select('my_pages', 'userid IS NOT NULL AND private = :private', ['private' => $private]);
// We should be good to go now.
$transaction->allow_commit();
}
}
},
- "grunt-contrib-jshint": {
- "version": "0.11.3",
- "from": "grunt-contrib-jshint@0.11.3",
- "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-0.11.3.tgz"
- },
"grunt-contrib-less": {
"version": "1.1.0",
"from": "grunt-contrib-less@1.1.0",
"dependencies": {
"lodash": {
"version": "4.13.1",
- "from": "lodash@>=4.3.0 <5.0.0"
+ "from": "lodash@4.13.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz"
}
}
},
"from": "jsbn@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz"
},
- "jshint": {
- "version": "2.8.0",
- "from": "jshint@>=2.8.0 <2.9.0",
- "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.8.0.tgz",
- "dependencies": {
- "lodash": {
- "version": "3.7.0",
- "from": "lodash@>=3.7.0 <3.8.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz"
- },
- "minimatch": {
- "version": "2.0.10",
- "from": "minimatch@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz"
- }
- }
- },
"jslint": {
"version": "0.3.4",
"from": "jslint@>=0.3.0 <0.4.0",
"dependencies": {
"lodash": {
"version": "4.13.1",
- "from": "lodash@>=4.0.0 <5.0.0"
+ "from": "lodash@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz"
}
}
},
"devDependencies": {
"async": "^1.5.2",
"grunt": "0.4.5",
- "grunt-contrib-jshint": "0.11.3",
"grunt-contrib-less": "1.1.0",
"grunt-contrib-uglify": "0.11.0",
"grunt-contrib-watch": "0.6.1",
$string['choices'] = 'Available choices';
$string['clozeaid'] = 'Enter missing word';
$string['correctansweris'] = 'The correct answer is: {$a}';
+$string['correctanswersare'] = 'The correct answers are: {$a}';
$string['correctfeedback'] = 'For any correct response';
$string['deletedchoice'] = 'This choice was deleted after the attempt was started.';
$string['errgradesetanswerblank'] = 'Grade set, but the Answer is blank';
public function specific_feedback(question_attempt $qa) {
return $this->combined_feedback($qa);
}
+
+ /**
+ * Function returns string based on number of correct answers
+ * @param array $right An Array of correct responses to the current question
+ * @return string based on number of correct responses
+ */
+ protected function correct_choices(array $right) {
+ // Return appropriate string for single/multiple correct answer(s).
+ if (count($right) == 1) {
+ return get_string('correctansweris', 'qtype_multichoice',
+ implode(', ', $right));
+ } else if (count($right) > 1) {
+ return get_string('correctanswersare', 'qtype_multichoice',
+ implode(', ', $right));
+ } else {
+ return "";
+ }
+ }
}
public function correct_response(question_attempt $qa) {
$question = $qa->get_question();
+ // Put all correct answers (100% grade) into $right.
+ $right = array();
foreach ($question->answers as $ansid => $ans) {
if (question_state::graded_state_for_fraction($ans->fraction) ==
question_state::$gradedright) {
- return get_string('correctansweris', 'qtype_multichoice',
- $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
- $qa, 'question', 'answer', $ansid)));
+ $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
+ $qa, 'question', 'answer', $ansid));
}
}
-
- return '';
+ return $this->correct_choices($right);
}
}
$qa, 'question', 'answer', $ansid));
}
}
-
- if (!empty($right)) {
- return get_string('correctansweris', 'qtype_multichoice',
- implode(', ', $right));
- }
- return '';
+ return $this->correct_choices($right);
}
protected function num_parts_correct(question_attempt $qa) {
And I should see "Mark 1.00 out of 1.00"
And I should see "Well done!"
And I should see "The odd numbers are One and Three."
- And I should see "The correct answer is: One, Three"
+ And I should see "The correct answers are: One, Three"
And I switch to the main window
@javascript @_switch_window
foreach ($searchareas as $areaid => $searcharea) {
$areanames[$areaid] = $searcharea->get_visible_name();
}
+
+ // Sort the array by the text.
+ \core_collator::asort($areanames);
+
$options = array(
'multiple' => true,
'noselectionstring' => get_string('allareas', 'search'),
--- /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/>.
+
+/**
+ * Search area for Users for whom I have authority to view profile.
+ *
+ * @package core_user
+ * @copyright 2016 Devang Gaur {@link http://www.devanggaur.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_user\search;
+
+require_once($CFG->dirroot . '/user/lib.php');
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for Users for whom I have access to view profile.
+ *
+ * @package core_user
+ * @copyright 2016 Devang Gaur {@link http://www.devanggaur.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user extends \core_search\area\base {
+
+ /**
+ * Returns recordset containing required data attributes for indexing.
+ *
+ * @param number $modifiedfrom
+ * @return \moodle_recordset
+ */
+ public function get_recordset_by_timestamp($modifiedfrom = 0) {
+ global $DB;
+ return $DB->get_recordset_select('user', 'timemodified >= ? AND deleted = ? AND
+ confirmed = ?', array($modifiedfrom, 0, 1));
+ }
+
+ /**
+ * Returns document instances for each record in the recordset.
+ *
+ * @param StdClass $record
+ * @param array $options
+ * @return core_search/document
+ */
+ public function get_document($record, $options = array()) {
+
+ $context = \context_system::instance();
+
+ // Prepare associative array with data from DB.
+ $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+ // Assigning properties to our document.
+ $doc->set('title', content_to_text(fullname($record), false));
+ $doc->set('contextid', $context->id);
+ $doc->set('courseid', SITEID);
+ $doc->set('itemid', $record->id);
+ $doc->set('modified', $record->timemodified);
+ $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+ $doc->set('content', content_to_text($record->description, $record->descriptionformat));
+
+ // Check if this document should be considered new.
+ if (isset($options['lastindexedtime']) && $options['lastindexedtime'] < $record->timecreated) {
+ // If the document was created after the last index time, it must be new.
+ $doc->set_is_new(true);
+ }
+
+ return $doc;
+ }
+
+ /**
+ * Checking whether I can access a document
+ *
+ * @param int $id user id
+ * @return int
+ */
+ public function check_access($id) {
+ global $DB, $USER;
+
+ $user = $DB->get_record('user', array('id' => $id));
+ if (!$user || $user->deleted) {
+ return \core_search\manager::ACCESS_DELETED;
+ }
+
+ if (user_can_view_profile($user)) {
+ return \core_search\manager::ACCESS_GRANTED;
+ }
+
+ return \core_search\manager::ACCESS_DENIED;
+ }
+
+ /**
+ * Returns a url to the profile page of user.
+ *
+ * @param \core_search\document $doc
+ * @return moodle_url
+ */
+ public function get_doc_url(\core_search\document $doc) {
+ return $this->get_context_url($doc);
+ }
+
+ /**
+ * Returns a url to the document context.
+ *
+ * @param \core_search\document $doc
+ * @return moodle_url
+ */
+ public function get_context_url(\core_search\document $doc) {
+ return new \moodle_url('/user/profile.php', array('id' => $doc->get('itemid')));
+ }
+}
--- /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/>.
+
+/**
+ * Course global search unit tests.
+ *
+ * @package core
+ * @copyright 2016 Devang Gaur {@link http://www.devanggaur.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
+
+/**
+ * Provides the unit tests for course global search.
+ *
+ * @package core
+ * @copyright 2016 Devang Gaur {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_search_testcase extends advanced_testcase {
+
+ /**
+ * @var string Area id
+ */
+ protected $userareaid = null;
+
+ public function setUp() {
+ $this->resetAfterTest(true);
+ set_config('enableglobalsearch', true);
+
+ $this->userareaid = \core_search\manager::generate_areaid('core_user', 'user');
+
+ // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
+ $search = testable_core_search::instance();
+ }
+
+ /**
+ * Indexing users contents.
+ *
+ * @return void
+ */
+ public function test_users_indexing() {
+
+ // Returns the instance as long as the area is supported.
+ $searcharea = \core_search\manager::get_search_area($this->userareaid);
+ $this->assertInstanceOf('\core_user\search\user', $searcharea);
+
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+
+ // All records.
+ // Recordset will produce 4 user records:
+ // Guest User, Admin User and two above generated users.
+ $recordset = $searcharea->get_recordset_by_timestamp(0);
+ $this->assertTrue($recordset->valid());
+ $nrecords = 0;
+ foreach ($recordset as $record) {
+ $this->assertInstanceOf('stdClass', $record);
+ $doc = $searcharea->get_document($record);
+ $this->assertInstanceOf('\core_search\document', $doc);
+ $nrecords++;
+ }
+ // If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
+ $recordset->close();
+ $this->assertEquals(4, $nrecords);
+
+ // The +2 is to prevent race conditions.
+ $recordset = $searcharea->get_recordset_by_timestamp(time() + 2);
+
+ // No new records.
+ $this->assertFalse($recordset->valid());
+ $recordset->close();
+ }
+
+ /**
+ * Document contents.
+ *
+ * @return void
+ */
+ public function test_users_document() {
+
+ // Returns the instance as long as the area is supported.
+ $searcharea = \core_search\manager::get_search_area($this->userareaid);
+ $this->assertInstanceOf('\core_user\search\user', $searcharea);
+
+ $user = self::getDataGenerator()->create_user();
+
+ $doc = $searcharea->get_document($user);
+ $this->assertInstanceOf('\core_search\document', $doc);
+ $this->assertEquals($user->id, $doc->get('itemid'));
+ $this->assertEquals($this->userareaid . '-' . $user->id, $doc->get('id'));
+ $this->assertEquals(SITEID, $doc->get('courseid'));
+ $this->assertFalse($doc->is_set('userid'));
+ $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+ $this->assertEquals(content_to_text(fullname($user), false), $doc->get('title'));
+ $this->assertEquals(content_to_text($user->description, $user->descriptionformat), $doc->get('content'));
+ }
+
+ /**
+ * Document accesses.
+ *
+ * @return void
+ */
+ public function test_users_access() {
+
+ // Returns the instance as long as the area is supported.
+ $searcharea = \core_search\manager::get_search_area($this->userareaid);
+
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+ $user3 = self::getDataGenerator()->create_user();
+ $user4 = self::getDataGenerator()->create_user();
+
+ $deleteduser = self::getDataGenerator()->create_user(array('deleted' => 1));
+ $unconfirmeduser = self::getDataGenerator()->create_user(array('confirmed' => 0));
+ $suspendeduser = self::getDataGenerator()->create_user(array('suspended' => 1));
+
+ $course1 = self::getDataGenerator()->create_course();
+ $course2 = self::getDataGenerator()->create_course();
+
+ $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
+ $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
+
+ $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
+ $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
+ $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
+ $this->getDataGenerator()->enrol_user($user3->id, $course2->id, 'student');
+ $this->getDataGenerator()->enrol_user($user4->id, $course2->id, 'student');
+ $this->getDataGenerator()->enrol_user($suspendeduser->id, $course1->id, 'student');
+
+ $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group1->id));
+ $this->getDataGenerator()->create_group_member(array('userid' => $user3->id, 'groupid' => $group1->id));
+ $this->getDataGenerator()->create_group_member(array('userid' => $user4->id, 'groupid' => $group2->id));
+
+ $this->setAdminUser();
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id));
+ $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access($deleteduser->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($unconfirmeduser->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($suspendeduser->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access(2));
+
+ $this->setUser($user1);
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id));
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user3->id));
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user4->id));
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access(1));// Guest user can't be accessed.
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access(2));// Admin user can't be accessed.
+ $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123));
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($unconfirmeduser->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($suspendeduser->id));
+
+ $this->setUser($user2);
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user4->id));
+
+ $this->setUser($user3);
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user1->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id));
+ $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($suspendeduser->id));
+
+ $this->setGuestUser();
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id));
+ $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id));
+ }
+}
\ No newline at end of file
throw new moodle_exception('nofile');
}
}
+
+ // Scan for viruses.
+ \core\antivirus\manager::scan_file($_FILES[$fieldname]['tmp_name'], $_FILES[$fieldname]['name'], true);
+
$file = new stdClass();
$file->filename = clean_param($_FILES[$fieldname]['name'], PARAM_FILE);
// check system maxbytes setting