Merge branch 'MDL-53865-master' of git://github.com/junpataleta/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 26 Apr 2016 01:38:33 +0000 (09:38 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 26 Apr 2016 01:38:33 +0000 (09:38 +0800)
254 files changed:
admin/dataformats.php [new file with mode: 0644]
admin/environment.xml
admin/settings/plugins.php
admin/tool/log/store/standard/db/install.xml
admin/tool/log/store/standard/db/upgrade.php
admin/tool/log/store/standard/version.php
admin/tool/lp/amd/build/coursecompetencies.min.js [deleted file]
admin/tool/lp/amd/build/frameworkdelete.min.js [deleted file]
admin/tool/lp/amd/build/menu.min.js [deleted file]
admin/tool/lp/amd/build/plandelete.min.js [deleted file]
admin/tool/lp/classes/course_competencies_form_element.php
admin/tool/lp/lib.php
admin/tool/lp/pix/competency.svg
admin/tool/lp/settings.php
admin/tool/lpmigrate/classes/framework_processor.php
admin/tool/lpmigrate/settings.php
admin/tool/lpmigrate/tests/processor_test.php
admin/tool/mobile/classes/api.php [new file with mode: 0644]
admin/tool/mobile/classes/external.php [new file with mode: 0644]
admin/tool/mobile/db/services.php [new file with mode: 0644]
admin/tool/mobile/lang/en/tool_mobile.php [new file with mode: 0644]
admin/tool/mobile/tests/externallib_test.php [new file with mode: 0644]
admin/tool/mobile/version.php [new file with mode: 0644]
admin/tool/uploaduser/index.php
admin/tool/uploaduser/lang/en/tool_uploaduser.php
admin/tool/uploaduser/user_form.php
admin/user/user_bulk_download.php
admin/webservice/testclient_forms.php
auth/db/auth.php
auth/db/tests/db_test.php
auth/db/upgrade.txt
auth/ldap/tests/plugin_test.php
blocks/blog_menu/tests/behat/block_blog_menu_activity.feature [new file with mode: 0644]
blocks/lp/block_lp.php
blog/edit.php
blog/index.php
cache/stores/memcached/addinstanceform.php
cache/stores/memcached/lang/en/cachestore_memcached.php
cache/stores/memcached/lib.php
cache/stores/memcached/tests/memcached_test.php
calendar/view.php
competency/classes/api.php
competency/classes/course_module_competency.php
competency/classes/related_competency.php
competency/classes/template_competency.php
competency/classes/user_competency_course.php
competency/classes/user_competency_plan.php
competency/classes/user_evidence_competency.php
competency/lib.php
config-dist.php
dataformat/csv/classes/writer.php [new file with mode: 0644]
dataformat/csv/lang/en/dataformat_csv.php [new file with mode: 0644]
dataformat/csv/version.php [new file with mode: 0644]
dataformat/excel/classes/writer.php [new file with mode: 0644]
dataformat/excel/lang/en/dataformat_excel.php [new file with mode: 0644]
dataformat/excel/version.php [new file with mode: 0644]
dataformat/html/classes/writer.php [new file with mode: 0644]
dataformat/html/lang/en/dataformat_html.php [new file with mode: 0644]
dataformat/html/version.php [new file with mode: 0644]
dataformat/json/classes/writer.php [new file with mode: 0644]
dataformat/json/lang/en/dataformat_json.php [new file with mode: 0644]
dataformat/json/version.php [new file with mode: 0644]
dataformat/ods/classes/writer.php [new file with mode: 0644]
dataformat/ods/lang/en/dataformat_ods.php [new file with mode: 0644]
dataformat/ods/version.php [new file with mode: 0644]
dataformat/upgrade.txt [new file with mode: 0644]
enrol/manual/yui/quickenrolment/assets/skins/sam/quickenrolment.css
enrol/manual/yui/quickenrolment/quickenrolment.js
lang/en/admin.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/moodle.php
lang/en/plugin.php
lang/en/table.php
lib/adminlib.php
lib/behat/behat_base.php
lib/classes/component.php
lib/classes/dataformat/base.php [new file with mode: 0644]
lib/classes/dataformat/spout_base.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/classes/plugininfo/dataformat.php [new file with mode: 0644]
lib/classes/session/redis.php [new file with mode: 0644]
lib/classes/user.php
lib/dataformatlib.php [new file with mode: 0644]
lib/db/upgrade.php
lib/external/externallib.php
lib/external/tests/external_test.php
lib/grade/grade_category.php
lib/grade/grade_scale.php
lib/grade/tests/grade_category_test.php
lib/navigationlib.php
lib/outputrenderers.php
lib/questionlib.php
lib/spout/LICENSE [new file with mode: 0644]
lib/spout/README.md [new file with mode: 0644]
lib/spout/readme_moodle.txt [new file with mode: 0644]
lib/spout/src/Spout/Autoloader/Psr4Autoloader.php [new file with mode: 0644]
lib/spout/src/Spout/Autoloader/autoload.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Escaper/CSV.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Escaper/EscaperInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Escaper/ODS.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Escaper/XLSX.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/EncodingConversionException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/IOException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/InvalidArgumentException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/SpoutException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Exception/UnsupportedTypeException.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/EncodingHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/FileSystemHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Helper/StringHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Common/Type.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/AbstractReader.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/Reader.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/RowIterator.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/Sheet.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/SheetIterator.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/NoSheetsFoundException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/ReaderException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/ReaderNotOpenedException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/SharedStringNotFoundException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Exception/XMLProcessingException.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/IteratorInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Helper/CellValueFormatter.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Reader.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/RowIterator.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Sheet.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/SheetIterator.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ReaderFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ReaderInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/SheetInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Wrapper/SimpleXMLElement.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Wrapper/XMLReader.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/FileBasedStrategy.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Reader.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/RowIterator.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/Sheet.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/SheetIterator.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/AbstractWriter.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/CSV/Writer.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Helper/ZipHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Internal/WorksheetInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Common/Sheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/InvalidColorException.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/InvalidSheetNameException.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/SheetNotFoundException.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/WriterAlreadyOpenedException.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/WriterException.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Exception/WriterNotOpenedException.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/ODS/Writer.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Style/Color.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Style/Style.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/Style/StyleBuilder.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/WriterFactory.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/WriterInterface.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php [new file with mode: 0644]
lib/spout/src/Spout/Writer/XLSX/Writer.php [new file with mode: 0644]
lib/tablelib.php
lib/templates/dataformat_selector.mustache [new file with mode: 0644]
lib/testing/tests/generator_test.php
lib/tests/behat/behat_hooks.php
lib/tests/datalib_test.php
lib/tests/redis_session_test.php [new file with mode: 0644]
lib/tests/user_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
login/index.php
login/signup_form.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/gradingbatchoperationsform.php
mod/assign/gradingoptionsform.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/assign/settings.php
mod/assign/tests/locallib_test.php
mod/feedback/analysis_course.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/observer.php
mod/forum/externallib.php
mod/forum/lib.php
mod/forum/tests/externallib_test.php
mod/lti/service/toolsettings/classes/local/resource/contextsettings.php
mod/lti/service/toolsettings/classes/local/resource/linksettings.php
mod/lti/service/toolsettings/classes/local/resource/systemsettings.php
mod/lti/service/toolsettings/classes/local/service/toolsettings.php
mod/lti/service/toolsettings/version.php
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/report.php
mod/quiz/styles.css
mod/resource/mod_form.php
mod/url/mod_form.php
mod/wiki/db/tag.php
mod/wiki/locallib.php
mod/wiki/tests/generator/lib.php
mod/wiki/tests/generator_test.php
mod/wiki/tests/lib_test.php
mod/wiki/version.php
pix/t/editinline.svg
report/competency/lib.php
report/log/classes/renderer.php
repository/merlot/lib.php
search/classes/document.php
search/classes/engine.php
search/classes/manager.php
search/engine/solr/classes/engine.php
search/engine/solr/classes/schema.php
search/engine/solr/lang/en/search_solr.php
search/engine/solr/setup_schema.php
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/pix/mod/quiz/checkmark.png [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/checkmark.svg [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/flag-on.png [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/flag-on.svg [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/warningtriangle.png [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/warningtriangle.svg [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/whitecircle.png [new file with mode: 0644]
theme/bootstrapbase/pix/mod/quiz/whitecircle.svg [new file with mode: 0644]
theme/bootstrapbase/style/moodle.css
user/editadvanced_form.php
user/editlib.php
user/externallib.php
user/forum_form.php
user/index.php
user/language.php
user/language_form.php
user/lib.php
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php

diff --git a/admin/dataformats.php b/admin/dataformats.php
new file mode 100644 (file)
index 0000000..f531e9b
--- /dev/null
@@ -0,0 +1,82 @@
+<?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/>.
+
+/**
+ * Lets users manage data formats
+ *
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @package    core
+ * @subpackage dataformat
+ */
+
+require_once('../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$action = required_param('action', PARAM_ALPHANUMEXT);
+$name   = required_param('name', PARAM_PLUGIN);
+
+$syscontext = context_system::instance();
+$PAGE->set_url('/admin/dataformats.php');
+$PAGE->set_context($syscontext);
+
+require_login();
+require_capability('moodle/site:config', $syscontext);
+require_sesskey();
+
+$return = new moodle_url('/admin/settings.php', array('section' => 'managedataformats'));
+
+$plugins = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
+$sortorder = array_flip(array_keys($plugins));
+
+if (!isset($plugins[$name])) {
+    print_error('courseformatnotfound', 'error', $return, $name);
+}
+
+switch ($action) {
+    case 'disable':
+        if ($plugins[$name]->is_enabled()) {
+            set_config('disabled', 1, 'dataformat_'. $name);
+            core_plugin_manager::reset_caches();
+        }
+        break;
+    case 'enable':
+        if (!$plugins[$name]->is_enabled()) {
+            unset_config('disabled', 'dataformat_'. $name);
+            core_plugin_manager::reset_caches();
+        }
+        break;
+    case 'up':
+        if ($sortorder[$name]) {
+            $currentindex = $sortorder[$name];
+            $seq = array_keys($plugins);
+            $seq[$currentindex] = $seq[$currentindex - 1];
+            $seq[$currentindex - 1] = $name;
+            set_config('dataformat_plugins_sortorder', implode(',', $seq));
+        }
+        break;
+    case 'down':
+        if ($sortorder[$name] < count($sortorder) - 1) {
+            $currentindex = $sortorder[$name];
+            $seq = array_keys($plugins);
+            $seq[$currentindex] = $seq[$currentindex + 1];
+            $seq[$currentindex + 1] = $name;
+            set_config('dataformat_plugins_sortorder', implode(',', $seq));
+        }
+        break;
+}
+redirect($return);
+
index 43bc5e4..1fc8ce1 100644 (file)
       </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" />
index 1bea1b2..1beaca5 100644 (file)
@@ -187,6 +187,11 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'filtersettings', $hassiteconfig);
     }
 
+    // Data format settings.
+    $ADMIN->add('modules', new admin_category('dataformatsettings', new lang_string('dataformats')));
+    $temp = new admin_settingpage('managedataformats', new lang_string('managedataformats'));
+    $temp->add(new admin_setting_managedataformats());
+    $ADMIN->add('dataformatsettings', $temp);
 
     //== Portfolio settings ==
     require_once($CFG->libdir. '/portfoliolib.php');
index 9586c03..22d80d4 100644 (file)
@@ -30,6 +30,7 @@
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="timecreated" UNIQUE="false" FIELDS="timecreated"/>
index b844b62..4e23361 100644 (file)
@@ -25,7 +25,9 @@
 defined('MOODLE_INTERNAL') || die();
 
 function xmldb_logstore_standard_upgrade($oldversion) {
-    global $CFG;
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
 
     // Moodle v2.8.0 release upgrade line.
     // Put any upgrade step following this.
@@ -36,5 +38,20 @@ function xmldb_logstore_standard_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016041200) {
+        // This could take a long time. Unfortunately, no way to know how long, and no way to do progress, so setting for 1 hour.
+        upgrade_set_timeout(3600);
+
+        // Define key contextid (foreign) to be added to logstore_standard_log.
+        $table = new xmldb_table('logstore_standard_log');
+        $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
+
+        // Launch add key contextid.
+        $dbman->add_key($table, $key);
+
+        // Standard savepoint reached.
+        upgrade_plugin_savepoint(true, 2016041200, 'logstore', 'standard');
+    }
+
     return true;
 }
index 73c131e..2e482f4 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2015111600; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2016041200; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2015111000; // Requires this Moodle version.
 $plugin->component = 'logstore_standard'; // Full name of the plugin (used for diagnostics).
diff --git a/admin/tool/lp/amd/build/coursecompetencies.min.js b/admin/tool/lp/amd/build/coursecompetencies.min.js
deleted file mode 100644 (file)
index 2fe295f..0000000
Binary files a/admin/tool/lp/amd/build/coursecompetencies.min.js and /dev/null differ
diff --git a/admin/tool/lp/amd/build/frameworkdelete.min.js b/admin/tool/lp/amd/build/frameworkdelete.min.js
deleted file mode 100644 (file)
index d1a3e8c..0000000
Binary files a/admin/tool/lp/amd/build/frameworkdelete.min.js and /dev/null differ
diff --git a/admin/tool/lp/amd/build/menu.min.js b/admin/tool/lp/amd/build/menu.min.js
deleted file mode 100644 (file)
index d530f21..0000000
Binary files a/admin/tool/lp/amd/build/menu.min.js and /dev/null differ
diff --git a/admin/tool/lp/amd/build/plandelete.min.js b/admin/tool/lp/amd/build/plandelete.min.js
deleted file mode 100644 (file)
index 318f73b..0000000
Binary files a/admin/tool/lp/amd/build/plandelete.min.js and /dev/null differ
index 035d2cd..4683e64 100644 (file)
@@ -75,6 +75,8 @@ class tool_lp_course_competencies_form_element extends MoodleQuickForm_autocompl
 
         $context = context_course::instance($courseid);
         foreach ($competencies as $competency) {
+            // We don't need to show the description as part of the options, so just set this to null.
+            $competency['competency']->set_description(null);
             $exporter = new competency_exporter($competency['competency'], array('context' => $context));
             $templatecontext = array('competency' => $exporter->export($OUTPUT));
             $id = $competency['competency']->get_id();
index 04a5dfe..1539e32 100644 (file)
@@ -32,7 +32,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param context $coursecontext The context of the course
  */
 function tool_lp_extend_navigation_course($navigation, $course, $coursecontext) {
-    if (!\core_competency\api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return;
     }
 
@@ -61,7 +61,7 @@ function tool_lp_extend_navigation_course($navigation, $course, $coursecontext)
  * @param context_course $coursecontext The context of the course
  */
 function tool_lp_extend_navigation_user($navigation, $user, $usercontext, $course, $coursecontext) {
-    if (!\core_competency\api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return;
     }
 
@@ -88,7 +88,7 @@ function tool_lp_extend_navigation_user($navigation, $user, $usercontext, $cours
  * @return bool
  */
 function tool_lp_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) {
-    if (!\core_competency\api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return false;
     } else if (!\core_competency\plan::can_read_user($user->id)) {
         return false;
@@ -109,7 +109,7 @@ function tool_lp_myprofile_navigation(core_user\output\myprofile\tree $tree, $us
  * @param context $coursecategorycontext The context of the course category
  */
 function tool_lp_extend_navigation_category_settings($navigation, $coursecategorycontext) {
-    if (!\core_competency\api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return false;
     }
 
@@ -160,7 +160,7 @@ function tool_lp_extend_navigation_category_settings($navigation, $coursecategor
 function tool_lp_coursemodule_standard_elements($formwrapper, $mform) {
     global $CFG, $COURSE;
 
-    if (!\core_competency\api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return;
     } else if (!has_capability('moodle/competency:coursecompetencymanage', $formwrapper->get_context())) {
         return;
@@ -195,7 +195,7 @@ function tool_lp_coursemodule_standard_elements($formwrapper, $mform) {
  * @param stdClass $course The course.
  */
 function tool_lp_coursemodule_edit_post_actions($data, $course) {
-    if (!\core_competency\api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return $data;
     }
 
index 2f845ce..e5f670e 100644 (file)
@@ -14,7 +14,7 @@
    id="svg2"
    version="1.1"
    inkscape:version="0.48.4 r9939"
-   sodipodi:docname="New document 1">
+   sodipodi:docname="New document 1" preserveAspectRatio="xMinYMid meet">
   <defs
      id="defs4" />
   <sodipodi:namedview
index 80758ae..2481791 100644 (file)
@@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 $parentname = 'competencies';
 
 // If the plugin is enabled we add the pages.
-if (\core_competency\api::is_enabled()) {
+if (get_config('core_competency', 'enabled')) {
 
     // Manage competency frameworks page.
     $temp = new admin_externalpage(
index 1f06438..0dac5e7 100644 (file)
@@ -543,6 +543,7 @@ class framework_processor {
 
                             } catch (moodle_exception $e) {
                                 // There was a major problem with this competency in this module.
+                                $competencieswithissues[$competencyid] = true;
                                 $message = get_string('errorwhilemigratingmodulecompetencywithexception', 'tool_lpmigrate',
                                     $e->getMessage());
                                 $this->log_error($courseid, $competencyid, $cmid, $message);
@@ -564,6 +565,7 @@ class framework_processor {
                                 $this->modulecompetencyremovals++;
                             }
                         } catch (moodle_exception $e) {
+                            $competencieswithissues[$competencyid] = true;
                             $this->log_warning($courseid, $competencyid, $cmid,
                                 get_string('warningcouldnotremovemodulecompetency', 'tool_lpmigrate'));
                         }
@@ -574,6 +576,11 @@ class framework_processor {
             // Finally, we remove the course competencies, but only for the 100% successful ones.
             foreach ($competenciestoremovefromcourse as $competencyid => $unused) {
 
+                // Skip competencies with issues.
+                if (isset($competencieswithissues[$competencyid])) {
+                    continue;
+                }
+
                 try {
                     // Process the course competency.
                     api::remove_competency_from_course($courseid, $competencyid);
index 26b3622..b1ecf3b 100644 (file)
@@ -23,7 +23,7 @@
  */
 defined('MOODLE_INTERNAL') || die();
 
-if (\core_competency\api::is_enabled()) {
+if (get_config('core_competency', 'enabled')) {
 
     $parentname = 'competencies';
 
index 441c446..792a902 100644 (file)
@@ -332,7 +332,7 @@ class tool_lpmigrate_framework_processor_testcase extends advanced_testcase {
         $this->assertEquals(2, $processor->get_courses_found_count());
         $this->assertEquals(5, $processor->get_expected_course_competency_migrations());
         $this->assertEquals(3, $processor->get_course_competency_migrations());
-        $this->assertEquals(3, $processor->get_course_competency_removals());
+        $this->assertEquals(2, $processor->get_course_competency_removals());
 
         $this->assertEquals(3, $processor->get_cms_found_count());
         $this->assertEquals(5, $processor->get_expected_module_competency_migrations());
@@ -456,7 +456,7 @@ class tool_lpmigrate_framework_processor_testcase extends advanced_testcase {
         $this->assertEquals(1, $processor->get_courses_found_count());
         $this->assertEquals(2, $processor->get_expected_course_competency_migrations());
         $this->assertEquals(2, $processor->get_course_competency_migrations());
-        $this->assertEquals(2, $processor->get_course_competency_removals());
+        $this->assertEquals(0, $processor->get_course_competency_removals());
 
         $this->assertEquals(1, $processor->get_cms_found_count());
         $this->assertEquals(2, $processor->get_expected_module_competency_migrations());
@@ -472,8 +472,13 @@ class tool_lpmigrate_framework_processor_testcase extends advanced_testcase {
         $this->assertRegexp('/Sorry, but you do not currently have permissions to do that/', $errors[0]['message']);
         $this->assertEquals($this->f1comps['A3']->get_id(), $errors[1]['competencyid']);
 
-        $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A2'], $this->f2comps['A2']);
-        $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A3'], $this->f2comps['A3']);
+        // The new competencies were added to the course, but the old ones were not removed because they are still in modules.
+        $this->assertCourseCompetencyExists($this->c2, $this->f1comps['A2']);
+        $this->assertCourseCompetencyExists($this->c2, $this->f1comps['A3']);
+        $this->assertCourseCompetencyExists($this->c2, $this->f2comps['A2']);
+        $this->assertCourseCompetencyExists($this->c2, $this->f2comps['A3']);
+
+        // Module competencies were not migrated because permissions are lacking.
         $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A2'], $this->f2comps['A2']);
         $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A3'], $this->f2comps['A2']);
     }
diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php
new file mode 100644 (file)
index 0000000..c4ff370
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * Class for Moodle Mobile tools.
+ *
+ * @package    tool_mobile
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+namespace tool_mobile;
+
+use core_component;
+use core_plugin_manager;
+
+/**
+ * API exposed by tool_mobile
+ *
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+class api {
+
+    /**
+     * Returns a list of Moodle plugins supporting the mobile app.
+     *
+     * @return array an array of objects containing the plugin information
+     */
+    public static function get_plugins_supporting_mobile() {
+        global $CFG;
+        require_once($CFG->libdir . '/adminlib.php');
+
+        $pluginsinfo = [];
+        $plugintypes = core_component::get_plugin_types();
+
+        foreach ($plugintypes as $plugintype => $unused) {
+            // We need to include files here.
+            $pluginswithfile = core_component::get_plugin_list_with_file($plugintype, 'db' . DIRECTORY_SEPARATOR . 'mobile.php');
+            foreach ($pluginswithfile as $plugin => $notused) {
+                $path = core_component::get_plugin_directory($plugintype, $plugin);
+                $component = $plugintype . '_' . $plugin;
+                $version = get_component_version($component);
+
+                require_once("$path/db/mobile.php");
+                foreach ($addons as $addonname => $addoninfo) {
+                    $plugininfo = array(
+                        'component' => $component,
+                        'version' => $version,
+                        'addon' => $addonname,
+                        'dependencies' => !empty($addon['dependencies']) ? $addoninfo['dependencies'] : array(),
+                        'fileurl' => '',
+                        'filehash' => '',
+                        'filesize' => 0
+                    );
+
+                    // All the mobile packages must be under the plugin mobile directory.
+                    $package = $path . DIRECTORY_SEPARATOR . 'mobile' . DIRECTORY_SEPARATOR . $addonname . '.zip';
+                    if (file_exists($package)) {
+                        $plugininfo['fileurl'] = $CFG->wwwroot . '' . str_replace($CFG->dirroot, '', $package);
+                        $plugininfo['filehash'] = sha1_file($package);
+                        $plugininfo['filesize'] = filesize($package);
+                    }
+                    $pluginsinfo[] = $plugininfo;
+                }
+            }
+        }
+        return $pluginsinfo;
+    }
+
+}
diff --git a/admin/tool/mobile/classes/external.php b/admin/tool/mobile/classes/external.php
new file mode 100644 (file)
index 0000000..ca5fb71
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external API for this tool.
+ *
+ * @package    tool_mobile
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_mobile;
+
+require_once("$CFG->libdir/externallib.php");
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+use external_multiple_structure;
+use external_warnings;
+
+/**
+ * This is the external API for this tool.
+ *
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+    /**
+     * Returns description of get_plugins_supporting_mobile() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.1
+     */
+    public static function get_plugins_supporting_mobile_parameters() {
+        return new external_function_parameters(array());
+    }
+
+    /**
+     * Returns a list of Moodle plugins supporting the mobile app.
+     *
+     * @return array an array of warnings and objects containing the plugin information
+     * @since  Moodle 3.1
+     */
+    public static function get_plugins_supporting_mobile() {
+        return array(
+            'plugins' => api::get_plugins_supporting_mobile(),
+            'warnings' => array(),
+        );
+    }
+
+    /**
+     * Returns description of get_plugins_supporting_mobile() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.1
+     */
+    public static function get_plugins_supporting_mobile_returns() {
+        return new external_single_structure(
+            array(
+                'plugins' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'component' => new external_value(PARAM_COMPONENT, 'The plugin component name.'),
+                            'version' => new external_value(PARAM_NOTAGS, 'The plugin version number.'),
+                            'addon' => new external_value(PARAM_COMPONENT, 'The Mobile addon (package) name.'),
+                            'dependencies' => new external_multiple_structure(
+                                                new external_value(PARAM_COMPONENT, 'Mobile addon name.'),
+                                                'The list of Mobile addons this addon depends on.'
+                                               ),
+                            'fileurl' => new external_value(PARAM_URL, 'The addon package url for download
+                                                            or empty if it doesn\'t exist.'),
+                            'filehash' => new external_value(PARAM_RAW, 'The addon package hash or empty if it doesn\'t exist.'),
+                            'filesize' => new external_value(PARAM_INT, 'The addon package size or empty if it doesn\'t exist.')
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+}
diff --git a/admin/tool/mobile/db/services.php b/admin/tool/mobile/db/services.php
new file mode 100644 (file)
index 0000000..f7ddd74
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * Moodle Mobile tools webservice definitions.
+ *
+ *
+ * @package    tool_mobile
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$functions = array(
+
+    'tool_mobile_get_plugins_supporting_mobile' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'get_plugins_supporting_mobile',
+        'description' => 'Returns a list of Moodle plugins supporting the mobile app.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    )
+
+);
+
diff --git a/admin/tool/mobile/lang/en/tool_mobile.php b/admin/tool/mobile/lang/en/tool_mobile.php
new file mode 100644 (file)
index 0000000..56a1360
--- /dev/null
@@ -0,0 +1,25 @@
+<?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/>.
+
+/**
+ * Strings for component 'tool_mobile', language 'en'
+ *
+ * @package    tool_mobile
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Moodle Mobile tools';
diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..166fe5d
--- /dev/null
@@ -0,0 +1,56 @@
+<?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/>.
+
+/**
+ * Moodle Mobile admin tool external functions tests.
+ *
+ * @package    tool_mobile
+ * @category   external
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+use tool_mobile\external;
+
+/**
+ * External learning plans webservice API tests.
+ *
+ * @package     tool_mobile
+ * @copyright   2016 Juan Leyva
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since       Moodle 3.1
+ */
+class tool_mobile_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test get_plugins_supporting_mobile.
+     * This is a very basic test because currently there aren't plugins supporting Mobile in core.
+     */
+    public function test_get_plugins_supporting_mobile() {
+        $result = external::get_plugins_supporting_mobile();
+        $result = external_api::clean_returnvalue(external::get_plugins_supporting_mobile_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(0, $result['plugins']);
+    }
+
+}
diff --git a/admin/tool/mobile/version.php b/admin/tool/mobile/version.php
new file mode 100644 (file)
index 0000000..8028385
--- /dev/null
@@ -0,0 +1,28 @@
+<?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/>.
+
+/**
+ * Plugin version info
+ *
+ * @package    tool_mobile
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+$plugin->version   = 2016032401; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2016032400.00; // Requires this Moodle version.
+$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
index b053388..aaab461 100644 (file)
@@ -212,7 +212,7 @@ if ($formdata = $mform2->is_cancelled()) {
     // init upload progress tracker
     $upt = new uu_progress_tracker();
     $upt->start(); // start table
-
+    $validation = array();
     while ($line = $cir->next()) {
         $upt->flush();
         $linenum++;
@@ -280,7 +280,7 @@ if ($formdata = $mform2->is_cancelled()) {
         // normalize username
         $originalusername = $user->username;
         if ($standardusernames) {
-            $user->username = clean_param($user->username, PARAM_USERNAME);
+            $user->username = core_user::clean_field($user->username, 'username');
         }
 
         // make sure we really have username
@@ -295,7 +295,7 @@ if ($formdata = $mform2->is_cancelled()) {
             continue;
         }
 
-        if ($user->username !== clean_param($user->username, PARAM_USERNAME)) {
+        if ($user->username !== core_user::clean_field($user->username, 'username')) {
             $upt->track('status', get_string('invalidusername', 'error', 'username'), 'error');
             $upt->track('username', $errorstr, 'error');
             $userserrors++;
@@ -443,7 +443,7 @@ if ($formdata = $mform2->is_cancelled()) {
             }
 
             if ($standardusernames) {
-                $oldusername = clean_param($user->oldusername, PARAM_USERNAME);
+                $oldusername = core_user::clean_field($user->oldusername, 'username');
             } else {
                 $oldusername = $user->oldusername;
             }
@@ -597,7 +597,7 @@ if ($formdata = $mform2->is_cancelled()) {
                             if (empty($user->lang)) {
                                 // Do not change to not-set value.
                                 continue;
-                            } else if (clean_param($user->lang, PARAM_LANG) === '') {
+                            } else if (core_user::clean_field($user->lang, 'lang') === '') {
                                 $upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning');
                                 continue;
                             }
@@ -774,7 +774,7 @@ if ($formdata = $mform2->is_cancelled()) {
 
             if (empty($user->lang)) {
                 $user->lang = '';
-            } else if (clean_param($user->lang, PARAM_LANG) === '') {
+            } else if (core_user::clean_field($user->lang, 'lang') === '') {
                 $upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning');
                 $user->lang = '';
             }
@@ -1115,9 +1115,14 @@ if ($formdata = $mform2->is_cancelled()) {
                 }
             }
         }
+        $validation[$user->username] = core_user::validate($user);
     }
     $upt->close(); // close table
-
+    if (!empty($validation)) {
+        foreach ($validation as $username => $error) {
+            \core\notification::warning(get_string('invaliduserdata', 'tool_uploaduser', s($username)));
+        }
+    }
     $cir->close();
     $cir->cleanup(true);
 
@@ -1177,7 +1182,7 @@ while ($linenum <= $previewrows and $fields = $cir->next()) {
     $rowcols['status'] = array();
 
     if (isset($rowcols['username'])) {
-        $stdusername = clean_param($rowcols['username'], PARAM_USERNAME);
+        $stdusername = core_user::clean_field($rowcols['username'], 'username');
         if ($rowcols['username'] !== $stdusername) {
             $rowcols['status'][] = get_string('invalidusernameupload');
         }
index c39f476..9aafcfc 100644 (file)
@@ -33,6 +33,7 @@ $string['deleteerrors'] = 'Delete errors';
 $string['encoding'] = 'Encoding';
 $string['errormnetadd'] = 'Can not add remote users';
 $string['errors'] = 'Errors';
+$string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
 $string['nochanges'] = 'No changes';
 $string['pluginname'] = 'User upload';
 $string['renameerrors'] = 'Rename errors';
index 218392e..dfd2123 100644 (file)
@@ -226,28 +226,28 @@ class admin_uploaduser_form2 extends moodleform {
 
         $choices = array(0 => get_string('emaildisplayno'), 1 => get_string('emaildisplayyes'), 2 => get_string('emaildisplaycourse'));
         $mform->addElement('select', 'maildisplay', get_string('emaildisplay'), $choices);
-        $mform->setDefault('maildisplay', $CFG->defaultpreference_maildisplay);
+        $mform->setDefault('maildisplay', core_user::get_property_default('maildisplay'));
 
         $choices = array(0 => get_string('textformat'), 1 => get_string('htmlformat'));
         $mform->addElement('select', 'mailformat', get_string('emailformat'), $choices);
-        $mform->setDefault('mailformat', $CFG->defaultpreference_mailformat);
+        $mform->setDefault('mailformat', core_user::get_property_default('mailformat'));
         $mform->setAdvanced('mailformat');
 
         $choices = array(0 => get_string('emaildigestoff'), 1 => get_string('emaildigestcomplete'), 2 => get_string('emaildigestsubjects'));
         $mform->addElement('select', 'maildigest', get_string('emaildigest'), $choices);
-        $mform->setDefault('maildigest', $CFG->defaultpreference_maildigest);
+        $mform->setDefault('maildigest', core_user::get_property_default('maildigest'));
         $mform->setAdvanced('maildigest');
 
         $choices = array(1 => get_string('autosubscribeyes'), 0 => get_string('autosubscribeno'));
         $mform->addElement('select', 'autosubscribe', get_string('autosubscribe'), $choices);
-        $mform->setDefault('autosubscribe', $CFG->defaultpreference_autosubscribe);
+        $mform->setDefault('autosubscribe', core_user::get_property_default('autosubscribe'));
 
         $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="25"');
         $mform->setType('city', PARAM_TEXT);
         if (empty($CFG->defaultcity)) {
             $mform->setDefault('city', $templateuser->city);
         } else {
-            $mform->setDefault('city', $CFG->defaultcity);
+            $mform->setDefault('city', core_user::get_property_default('city'));
         }
 
         $choices = get_string_manager()->get_list_of_countries();
@@ -256,7 +256,7 @@ class admin_uploaduser_form2 extends moodleform {
         if (empty($CFG->country)) {
             $mform->setDefault('country', $templateuser->country);
         } else {
-            $mform->setDefault('country', $CFG->country);
+            $mform->setDefault('country', core_user::get_property_default('country'));
         }
         $mform->setAdvanced('country');
 
index 5d6ea51..08f5b32 100644 (file)
@@ -1,24 +1,44 @@
 <?php
-/**
-* script for downloading of user lists
-*/
+// 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/>.
 
+/**
+ * Bulk export user into any dataformat
+ *
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @copyright  2007 Petr Skoda
+ * @package    core
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
 require_once('../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/dataformatlib.php');
+require_once($CFG->dirroot.'/user/profile/lib.php');
 
-$format = optional_param('format', '', PARAM_ALPHA);
+$dataformat = optional_param('dataformat', '', PARAM_ALPHA);
 
 require_login();
 admin_externalpage_setup('userbulk');
 require_capability('moodle/user:update', context_system::instance());
 
-$return = $CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk.php';
-
 if (empty($SESSION->bulk_users)) {
-    redirect($return);
+    redirect(new moodle_url('/admin/user/user_bulk.php'));
 }
 
-if ($format) {
+if ($dataformat) {
     $fields = array('id'        => 'id',
                     'username'  => 'username',
                     'email'     => 'email',
@@ -39,142 +59,47 @@ if ($format) {
                     'country'   => 'country');
 
     if ($extrafields = $DB->get_records('user_info_field')) {
-        foreach ($extrafields as $n=>$v){
-            $fields['profile_field_'.$v->shortname] = 'profile_field_'.$v->shortname;
-        }
-    }
-
-    switch ($format) {
-        case 'csv' : user_download_csv($fields);
-        case 'ods' : user_download_ods($fields);
-        case 'xls' : user_download_xls($fields);
-
-    }
-    die;
-}
-
-echo $OUTPUT->header();
-echo $OUTPUT->heading(get_string('download', 'admin'));
-
-echo $OUTPUT->box_start();
-echo '<ul>';
-echo '<li><a href="user_bulk_download.php?format=csv">'.get_string('downloadtext').'</a></li>';
-echo '<li><a href="user_bulk_download.php?format=ods">'.get_string('downloadods').'</a></li>';
-echo '<li><a href="user_bulk_download.php?format=xls">'.get_string('downloadexcel').'</a></li>';
-echo '</ul>';
-echo $OUTPUT->box_end();
-
-echo $OUTPUT->continue_button($return);
-
-echo $OUTPUT->footer();
-
-function user_download_ods($fields) {
-    global $CFG, $SESSION, $DB;
-
-    require_once("$CFG->libdir/odslib.class.php");
-    require_once($CFG->dirroot.'/user/profile/lib.php');
-
-    $filename = clean_filename(get_string('users').'.ods');
-
-    $workbook = new MoodleODSWorkbook('-');
-    $workbook->send($filename);
-
-    $worksheet = array();
-
-    $worksheet[0] = $workbook->add_worksheet('');
-    $col = 0;
-    foreach ($fields as $fieldname) {
-        $worksheet[0]->write(0, $col, $fieldname);
-        $col++;
-    }
-
-    $row = 1;
-    foreach ($SESSION->bulk_users as $userid) {
-        if (!$user = $DB->get_record('user', array('id'=>$userid))) {
-            continue;
-        }
-        $col = 0;
-        profile_load_data($user);
-        foreach ($fields as $field=>$unused) {
-            $worksheet[0]->write($row, $col, $user->$field);
-            $col++;
+        foreach ($extrafields as $n => $field) {
+            $fields['profile_field_'.$field->shortname] = 'profile_field_'.$field->shortname;
+            require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
         }
-        $row++;
     }
 
-    $workbook->close();
-    die;
-}
-
-function user_download_xls($fields) {
-    global $CFG, $SESSION, $DB;
-
-    require_once("$CFG->libdir/excellib.class.php");
-    require_once($CFG->dirroot.'/user/profile/lib.php');
-
-    $filename = clean_filename(get_string('users').'.xls');
-
-    $workbook = new MoodleExcelWorkbook('-');
-    $workbook->send($filename);
-
-    $worksheet = array();
-
-    $worksheet[0] = $workbook->add_worksheet('');
-    $col = 0;
-    foreach ($fields as $fieldname) {
-        $worksheet[0]->write(0, $col, $fieldname);
-        $col++;
-    }
-
-    $row = 1;
-    foreach ($SESSION->bulk_users as $userid) {
-        if (!$user = $DB->get_record('user', array('id'=>$userid))) {
-            continue;
-        }
-        $col = 0;
-        profile_load_data($user);
-        foreach ($fields as $field=>$unused) {
-            $worksheet[0]->write($row, $col, $user->$field);
-            $col++;
-        }
-        $row++;
-    }
-
-    $workbook->close();
-    die;
-}
-
-function user_download_csv($fields) {
-    global $CFG, $SESSION, $DB;
-
-    require_once($CFG->dirroot.'/user/profile/lib.php');
-    require_once($CFG->libdir . '/csvlib.class.php');
-
     $filename = clean_filename(get_string('users'));
 
-    $csvexport = new csv_export_writer();
-    $csvexport->set_filename($filename);
-    $csvexport->add_data($fields);
+    $downloadusers = new ArrayObject($SESSION->bulk_users);
+    $iterator = $downloadusers->getIterator();
 
-    foreach ($SESSION->bulk_users as $userid) {
+    download_as_dataformat($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
+        global $DB;
         $row = array();
-        if (!$user = $DB->get_record('user', array('id'=>$userid))) {
-            continue;
+        if (!$user = $DB->get_record('user', array('id' => $userid))) {
+            return null;
+        }
+        foreach ($extrafields as $field) {
+            $newfield = 'profile_field_'.$field->datatype;
+            $formfield = new $newfield($field->id, $user->id);
+            $formfield->edit_load_user_data($user);
         }
-        profile_load_data($user);
         $userprofiledata = array();
-        foreach ($fields as $field=>$unused) {
+        foreach ($fields as $field => $unused) {
             // Custom user profile textarea fields come in an array
             // The first element is the text and the second is the format.
             // We only take the text.
             if (is_array($user->$field)) {
-                $userprofiledata[] = reset($user->$field);
+                $userprofiledata[$field] = reset($user->$field);
             } else {
-                $userprofiledata[] = $user->$field;
+                $userprofiledata[$field] = $user->$field;
             }
         }
-        $csvexport->add_data($userprofiledata);
-    }
-    $csvexport->download_file();
-    die;
+        return $userprofiledata;
+    });
+
+    exit;
 }
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('download', 'admin'));
+echo $OUTPUT->download_dataformat_selector(get_string('userbulkdownload', 'admin'), 'user_bulk_download.php');
+echo $OUTPUT->footer();
+
index ab88e23..cc958c2 100644 (file)
@@ -41,28 +41,28 @@ class moodle_user_create_users_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_SAFEDIR);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
 
         /// specific to the create users function
         $mform->addElement('text', 'username', 'username');
-        $mform->setType('username', PARAM_USERNAME);
+        $mform->setType('username', core_user::get_property_type('username'));
         $mform->addElement('text', 'password', 'password');
-        $mform->setType('password', PARAM_RAW);
+        $mform->setType('password', core_user::get_property_type('password'));
         $mform->addElement('text', 'firstname', 'firstname');
-        $mform->setType('firstname', PARAM_RAW);
+        $mform->setType('firstname', core_user::get_property_type('firstname'));
         $mform->addElement('text', 'lastname', 'lastname');
-        $mform->setType('lastname', PARAM_RAW);
+        $mform->setType('lastname', core_user::get_property_type('lastname'));
         $mform->addElement('text', 'email', 'email');
-        $mform->setType('email', PARAM_EMAIL);
+        $mform->setType('email', core_user::get_property_type('email'));
 
         $mform->addElement('text', 'customfieldtype', 'customfieldtype');
         $mform->setType('customfieldtype', PARAM_RAW);
@@ -125,31 +125,31 @@ class moodle_user_update_users_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
 
         /// specific to the create users function
         $mform->addElement('text', 'id', 'id');
         $mform->addRule('id', get_string('required'), 'required', null, 'client');
-        $mform->setType('id', PARAM_INT);
+        $mform->setType('id', core_user::get_property_type('id'));
         $mform->addElement('text', 'username', 'username');
-        $mform->setType('username', PARAM_USERNAME);
+        $mform->setType('username', core_user::get_property_type('username'));
         $mform->addElement('text', 'password', 'password');
-        $mform->setType('password', PARAM_RAW);
+        $mform->setType('password', core_user::get_property_type('password'));
         $mform->addElement('text', 'firstname', 'firstname');
-        $mform->setType('firstname', PARAM_RAW);
+        $mform->setType('firstname', core_user::get_property_type('firstname'));
         $mform->addElement('text', 'lastname', 'lastname');
-        $mform->setType('lastname', PARAM_RAW);
+        $mform->setType('lastname', core_user::get_property_type('lastname'));
         $mform->addElement('text', 'email', 'email');
-        $mform->setType('email', PARAM_EMAIL);
+        $mform->setType('email', core_user::get_property_type('email'));
 
 
         $mform->addElement('text', 'customfieldtype', 'customfieldtype');
@@ -219,23 +219,23 @@ class moodle_user_delete_users_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
 
         /// beginning of specific code to the create users function
         $mform->addElement('text', 'userids[0]', 'userids[0]');
         $mform->addElement('text', 'userids[1]', 'userids[1]');
         $mform->addElement('text', 'userids[2]', 'userids[2]');
         $mform->addElement('text', 'userids[3]', 'userids[3]');
-        $mform->setType('userids', PARAM_INT);
+        $mform->setType('userids', core_user::get_property_type('id'));
         /// end of specific code to the create users function
 
         $mform->addElement('hidden', 'function');
@@ -291,23 +291,23 @@ class moodle_user_get_users_by_id_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
 
         /// beginning of specific code to the create users function
         $mform->addElement('text', 'userids[0]', 'userids[0]');
         $mform->addElement('text', 'userids[1]', 'userids[1]');
         $mform->addElement('text', 'userids[2]', 'userids[2]');
         $mform->addElement('text', 'userids[3]', 'userids[3]');
-        $mform->setType('userids', PARAM_INT);
+        $mform->setType('userids', core_user::get_property_type('id'));
         /// end of specific code to the create users function
 
         $mform->addElement('hidden', 'function');
@@ -364,16 +364,16 @@ class moodle_group_create_groups_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
 
         $mform->addElement('text', 'courseid', 'courseid');
         $mform->setType('courseid', PARAM_INT);
@@ -430,16 +430,16 @@ class moodle_group_get_groups_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'groupids[0]', 'groupids[0]');
         $mform->addElement('text', 'groupids[1]', 'groupids[1]');
         $mform->addElement('text', 'groupids[2]', 'groupids[2]');
@@ -493,16 +493,16 @@ class moodle_group_get_course_groups_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'courseid', 'courseid');
 
         $mform->addElement('hidden', 'function');
@@ -546,16 +546,16 @@ class moodle_group_delete_groups_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'groupids[0]', 'groupids[0]');
         $mform->addElement('text', 'groupids[1]', 'groupids[1]');
         $mform->addElement('text', 'groupids[2]', 'groupids[2]');
@@ -611,16 +611,16 @@ class moodle_group_get_groupmembers_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'groupids[0]', 'groupids[0]');
         $mform->addElement('text', 'groupids[1]', 'groupids[1]');
         $mform->addElement('text', 'groupids[2]', 'groupids[2]');
@@ -674,21 +674,21 @@ class moodle_group_add_groupmembers_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_SAFEDIR);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'userid[0]', 'userid[0]');
         $mform->addElement('text', 'groupid[0]', 'groupid[0]');
         $mform->addElement('text', 'userid[1]', 'userid[1]');
         $mform->addElement('text', 'groupid[1]', 'groupid[1]');
-        $mform->setType('userid', PARAM_INT);
+        $mform->setType('userid', core_user::get_property_type('id'));
         $mform->setType('groupids', PARAM_INT);
 
         $mform->addElement('hidden', 'function');
@@ -738,16 +738,16 @@ class moodle_group_delete_groupmembers_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'userid[0]', 'userid[0]');
         $mform->addElement('text', 'groupid[0]', 'groupid[0]');
         $mform->addElement('text', 'userid[1]', 'userid[1]');
@@ -812,16 +812,16 @@ class core_course_create_categories_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'name[0]', 'name[0]');
         $mform->addElement('text', 'parent[0]', 'parent[0]');
         $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
@@ -830,10 +830,10 @@ class core_course_create_categories_form extends moodleform {
         $mform->addElement('text', 'parent[1]', 'parent[1]');
         $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
         $mform->addElement('text', 'description[1]', 'description[1]');
-        $mform->setType('name', PARAM_TEXT);
-        $mform->setType('parent', PARAM_INT);
-        $mform->setType('idnumber', PARAM_RAW);
-        $mform->setType('description', PARAM_TEXT);
+        $mform->setType('name', core_user::get_property_type('firstname'));
+        $mform->setType('parent', core_user::get_property_type('id'));
+        $mform->setType('idnumber', core_user::get_property_type('idnumber'));
+        $mform->setType('description', core_user::get_property_type('description'));
 
         $mform->addElement('hidden', 'function');
         $mform->setType('function', PARAM_PLUGIN);
@@ -896,23 +896,23 @@ class core_course_delete_categories_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'id[0]', 'id[0]');
         $mform->addElement('text', 'newparent[0]', 'newparent[0]');
         $mform->addElement('text', 'recursive[0]', 'recursive[0]');
         $mform->addElement('text', 'id[1]', 'id[1]');
         $mform->addElement('text', 'newparent[1]', 'newparent[1]');
         $mform->addElement('text', 'recursive[1]', 'recursive[1]');
-        $mform->setType('id', PARAM_INT);
+        $mform->setType('id', core_user::get_property_type('id'));
         $mform->setType('newparent', PARAM_INT);
         $mform->setType('recursive', PARAM_BOOL);
 
@@ -984,16 +984,16 @@ class core_course_update_categories_form extends moodleform {
         $data = $this->_customdata;
         if ($data['authmethod'] == 'simple') {
             $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', PARAM_USERNAME);
+            $mform->setType('wsusername', core_user::get_property_type('username'));
             $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', PARAM_RAW);
+            $mform->setType('wspassword', core_user::get_property_type('password'));
         } else if ($data['authmethod'] == 'token') {
             $mform->addElement('text', 'token', 'token');
             $mform->setType('token', PARAM_RAW_TRIMMED);
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', PARAM_ALPHA);
+        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'id[0]', 'id[0]');
         $mform->addElement('text', 'name[0]', 'name[0]');
         $mform->addElement('text', 'parent[0]', 'parent[0]');
@@ -1004,11 +1004,11 @@ class core_course_update_categories_form extends moodleform {
         $mform->addElement('text', 'parent[1]', 'parent[1]');
         $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
         $mform->addElement('text', 'description[1]', 'description[1]');
-        $mform->setType('id', PARAM_INT);
-        $mform->setType('name', PARAM_TEXT);
+        $mform->setType('id', core_user::get_property_type('id'));
+        $mform->setType('name', core_user::get_property_type('firstname'));
         $mform->setType('parent', PARAM_INT);
-        $mform->setType('idnumber', PARAM_RAW);
-        $mform->setType('description', PARAM_TEXT);
+        $mform->setType('idnumber', core_user::get_property_type('idnumber'));
+        $mform->setType('description', core_user::get_property_type('description'));
 
         $mform->addElement('hidden', 'function');
         $mform->setType('function', PARAM_PLUGIN);
index 8c2428d..ebebaa1 100644 (file)
@@ -328,7 +328,6 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id   = $user->id;
                         $updateuser->suspended = 1;
-                        $updateuser = $this->clean_data($updateuser);
                         user_update_user($updateuser, false);
                         $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1);
                     }
@@ -415,7 +414,6 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id = $olduser->id;
                         $updateuser->suspended = 0;
-                        $updateuser = $this->clean_data($updateuser);
                         user_update_user($updateuser);
                         $trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username,
                             'id' => $olduser->id)), 1);
@@ -438,7 +436,6 @@ class auth_plugin_db extends auth_plugin_base {
                     $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1);
                     continue;
                 }
-                $user = $this->clean_data($user);
                 try {
                     $id = user_create_user($user, false); // It is truly a new user.
                     $trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)), 1);
@@ -580,7 +577,6 @@ class auth_plugin_db extends auth_plugin_base {
         }
         if ($needsupdate) {
             require_once($CFG->dirroot . '/user/lib.php');
-            $updateuser = $this->clean_data($updateuser);
             user_update_user($updateuser);
         }
         return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
@@ -913,26 +909,14 @@ class auth_plugin_db extends auth_plugin_base {
 
     /**
      * Clean the user data that comes from an external database.
-     *
+     * @deprecated since 3.1, please use core_user::clean_data() instead.
      * @param array $user the user data to be validated against properties definition.
      * @return stdClass $user the cleaned user data.
      */
     public function clean_data($user) {
-        if (empty($user)) {
-            return $user;
-        }
-
-        foreach ($user as $field => $value) {
-            // Get the property parameter type and do the cleaning.
-            try {
-                $property = core_user::get_property_definition($field);
-                $user->$field = clean_param($value, $property['type']);
-            } catch (coding_exception $e) {
-                debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
-            }
-        }
-
-        return $user;
+        debugging('The method clean_data() has been deprecated, please use core_user::clean_data() instead.',
+            DEBUG_DEVELOPER);
+        return core_user::clean_data($user);
     }
 }
 
index e0d68d3..f1471cf 100644 (file)
@@ -121,7 +121,9 @@ class auth_db_testcase extends advanced_testcase {
         set_config('table', $CFG->prefix.'auth_db_users', 'auth/db');
         set_config('fielduser', 'name', 'auth/db');
         set_config('fieldpass', 'pass', 'auth/db');
-
+        set_config('field_map_lastname', 'lastname', 'auth/db');
+        set_config('field_updatelocal_lastname', 'oncreate', 'auth/db');
+        set_config('field_lock_lastname', 'unlocked', 'auth/db');
         // Setu up field mappings.
 
         set_config('field_map_email', 'email', 'auth/db');
@@ -149,7 +151,7 @@ class auth_db_testcase extends advanced_testcase {
     public function test_plugin() {
         global $DB, $CFG;
 
-        $this->resetAfterTest(false);
+        $this->resetAfterTest(true);
 
         // NOTE: It is strongly discouraged to create new tables in advanced_testcase classes,
         //       but there is no other simple way to test ext database enrol sync, so let's
@@ -416,60 +418,31 @@ class auth_db_testcase extends advanced_testcase {
         $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com');
         $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1);
 
-        // User with malicious data on the name.
+        // User with malicious data on the name (won't be imported).
         $extdbuser2 = (object)array('name'=>'user<script>alert(1);</script>xss', 'pass'=>'heslo', 'email'=>'xssuser@example.com');
         $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2);
 
+        $extdbuser3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com',
+                'lastname' => 'user<script>alert(1);</script>xss');
+        $extdbuser3->id = $DB->insert_record('auth_db_users', $extdbuser3);
         $trace = new null_progress_trace();
 
         // Let's test user sync make sure still works as expected..
         $auth->sync_users($trace, true);
-
-        // Get the user on moodle user table.
-        $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db'));
-
-        // The malicious code should be sanitized.
-        $this->assertEquals($user2->username, 'userscriptalert1scriptxss');
-        $this->assertNotEquals($user2->username, $extdbuser2->name);
-
+        $this->assertDebuggingCalled("The property 'lastname' has invalid data and has been cleaned.");
         // User with correct data, should be equal to external db.
         $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db'));
         $this->assertEquals($extdbuser1->name, $user1->username);
         $this->assertEquals($extdbuser1->email, $user1->email);
 
-        // Now, let's update the name.
-        $extdbuser2->name = 'user no xss anymore';
-        $DB->update_record('auth_db_users', $extdbuser2);
+        // Get the user on moodle user table.
+        $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db'));
+        $user3 = $DB->get_record('user', array('email'=> $extdbuser3->email, 'auth'=>'db'));
 
-        // Run sync again to update the user data.
-        $auth->sync_users($trace, true);
+        $this->assertEmpty($user2);
+        $this->assertEquals($extdbuser3->name, $user3->username);
+        $this->assertEquals('useralert(1);xss', $user3->lastname);
 
-        // The user information should be updated.
-        $user2 = $DB->get_record('user', array('username' => 'usernoxssanymore', 'auth' => 'db'));
-        // The spaces should be removed, as it's the username.
-        $this->assertEquals($user2->username, 'usernoxssanymore');
-
-        // Now let's test just the clean_data() method isolated.
-        // Testing PARAM_USERNAME, PARAM_NOTAGS, PARAM_RAW_TRIMMED and others.
-        $user3 = new stdClass();
-        $user3->firstname = 'John <script>alert(1)</script> Doe';
-        $user3->username = 'john%#&~%*_doe';
-        $user3->email = ' john@testing.com ';
-        $user3->deleted = 'no';
-        $user3->description = '<b>A description <script>alert(123)</script>about myself.</b>';
-        $user3cleaned = $auth->clean_data($user3);
-
-        // Expected results.
-        $this->assertEquals($user3cleaned->firstname, 'John alert(1) Doe');
-        $this->assertEquals($user3cleaned->email, 'john@testing.com');
-        $this->assertEquals($user3cleaned->deleted, 0);
-        $this->assertEquals($user3->description, '<b>A description about myself.</b>');
-        $this->assertEquals($user3->username, 'john_doe');
-
-        // Try to clean an invalid property (fullname).
-        $user3->fullname = 'John Doe';
-        $auth->clean_data($user3);
-        $this->assertDebuggingCalled("The property 'fullname' could not be cleaned.");
         $this->cleanup_auth_database();
     }
 }
index 45258e7..b9475fa 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /auth/db/*,
 information provided here is intended especially for developers.
 
+=== 3.1 ===
+
+* The auth_plugin_db::clean_data() has been deprecated and will be removed
+  in a future version. Please update to use core_user::clean_data()
+  instead.
+
 === 2.9 ===
 
 Some alterations have been made to the handling of case sensitity handling of passwords
index ba82adc..dff1485 100644 (file)
@@ -365,7 +365,7 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
             'email' => 'usersignuptest1@example.com',
             'description' => 'This is a description for user 1',
             'city' => 'Perth',
-            'country' => 'au',
+            'country' => 'AU',
             'mnethostid' => $CFG->mnet_localhost_id,
             'auth' => 'ldap'
             );
diff --git a/blocks/blog_menu/tests/behat/block_blog_menu_activity.feature b/blocks/blog_menu/tests/behat/block_blog_menu_activity.feature
new file mode 100644 (file)
index 0000000..7bfb063
--- /dev/null
@@ -0,0 +1,222 @@
+@block @block_blog_menu
+Feature: Enable Block blog menu in an activity
+  In order to enable the blog menu in an activity
+  As a teacher
+  I can add blog menu block to a course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+      | student2 | Student | 2 | student2@example.com | S2 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 1 |
+      | Description | Offline text |
+      | assignsubmission_file_enabled | 0 |
+    And I follow "Test assignment 1"
+    And I add the "Blog menu" block
+    And I log out
+
+  Scenario: Students use the blog menu block to post blogs
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add a new entry"
+    When I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    Then I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Blog entries"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+
+  Scenario: Students use the blog menu block to view their blogs about the activity
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this Assignment! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this Assignment!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    When I follow "View my entries about this Assignment"
+    Then I should see "S2 First Blog"
+    And I should not see "S2 Second Blog"
+    And I should not see "S1 First Blog"
+
+  Scenario: Students use the blog menu block to view all blogs about the assignment
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this Assignment! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this Assignment!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    When I follow "View all entries about this Assignment"
+    Then I should see "S1 First Blog"
+    And I should see "S2 First Blog"
+    And I should not see "S2 Second Blog"
+
+  Scenario: Students use the blog menu block to view all their blog entries
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this Assignment! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this Assignment!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    When I follow "Blog entries"
+    Then I should see "S2 First Blog"
+    And I should see "S2 Second Blog"
+    And I should not see "S1 First Blog"
+
+  Scenario: Teacher searches for student blogs
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog about this Assignment! |
+    And I press "Save changes"
+    And I should see "S1 First Blog"
+    And I should see "This is my awesome blog about this Assignment!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add a new entry"
+    And I set the following fields to these values:
+      | Entry title | S2 Second Blog |
+      | Blog entry body | My unrelated blog! |
+    And I press "Save changes"
+    And I should see "S2 Second Blog"
+    And I should see "My unrelated blog!"
+    And I should not see "Associated Assignment: Test assignment 1"
+    And I follow "Dashboard"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    And I set the following fields to these values:
+      | Entry title | S2 First Blog |
+      | Blog entry body | My course blog is better! |
+    And I press "Save changes"
+    And I should see "S2 First Blog"
+    And I should see "My course blog is better!"
+    And I should see "Associated Assignment: Test assignment 1"
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I set the field "blogsearchquery" to "First"
+    And I press "Search"
+    Then I should see "S1 First Blog"
+    And I should see "S2 First Blog"
+    And I should not see "S2 Second Blog"
\ No newline at end of file
index 84b02a0..60a9aea 100644 (file)
@@ -62,7 +62,7 @@ class block_lp extends block_base {
         }
         $this->content = new stdClass();
 
-        if (!\core_competency\api::is_enabled()) {
+        if (!get_config('core_competency', 'enabled')) {
             return $this->content;
         }
 
index 8248039..6b13994 100644 (file)
@@ -64,9 +64,13 @@ if ($id) {
 
 $sitecontext = context_system::instance();
 $usercontext = context_user::instance($userid);
-$PAGE->set_context($usercontext);
-$blognode = $PAGE->settingsnav->find('blogadd', null);
-$blognode->make_active();
+if ($modid) {
+    $PAGE->set_context($sitecontext);
+} else {
+    $PAGE->set_context($usercontext);
+    $blognode = $PAGE->settingsnav->find('blogadd', null);
+    $blognode->make_active();
+}
 
 require_login($courseid);
 
index 1b64b42..5aa84bd 100644 (file)
@@ -62,7 +62,7 @@ if ($entryid and !isset($userid)) {
     $userid = $entry->userid;
 }
 
-if (isset($userid) && empty($courseid)) {
+if (isset($userid) && empty($courseid) && empty($modid)) {
     $context = context_user::instance($userid);
 } else if (!empty($courseid) && $courseid != SITEID) {
     $context = context_course::instance($courseid);
index 12bfbbc..004063a 100644 (file)
@@ -41,7 +41,17 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
      * Adds the desired form elements.
      */
     protected function configuration_definition() {
+        global $OUTPUT;
+
         $form = $this->_form;
+        $version = phpversion('memcached');
+        $hasrequiredversion = ($version || version_compare($version, cachestore_memcached::REQUIRED_VERSION, '>='));
+
+        if (!$hasrequiredversion) {
+            $notify = new \core\output\notification(nl2br(get_string('upgrade200recommended', 'cachestore_memcached')),
+                \core\output\notification::NOTIFY_WARNING);
+            $form->addElement('html', $OUTPUT->render($notify));
+        }
 
         $form->addElement('textarea', 'servers', get_string('servers', 'cachestore_memcached'), array('cols' => 75, 'rows' => 5));
         $form->addHelpButton('servers', 'servers', 'cachestore_memcached');
@@ -75,6 +85,15 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
         $form->setDefault('bufferwrites', 0);
         $form->setType('bufferwrites', PARAM_BOOL);
 
+        if ($hasrequiredversion) {
+            // Only show this option if we have the required version of memcache extension installed.
+            // If it's not installed then this option does nothing, so there is no point in displaying it.
+            $form->addElement('selectyesno', 'isshared', get_string('isshared', 'cachestore_memcached'));
+            $form->addHelpButton('isshared', 'isshared', 'cachestore_memcached');
+            $form->setDefault('isshared', 0);
+            $form->setType('isshared', PARAM_BOOL);
+        }
+
         $form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcached'));
 
         $form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcached'));
index 27d079e..fcfb2dc 100644 (file)
@@ -46,6 +46,13 @@ $string['hash_fnv1_32'] = 'FNV1_32';
 $string['hash_fnv1a_32'] = 'FNV1A_32';
 $string['hash_hsieh'] = 'Hsieh';
 $string['hash_murmur'] = 'Murmur';
+$string['isshared'] = 'Shared cache';
+$string['isshared_help'] = "Is your memcached server also being used by other applications?
+
+If the cache is shared by other applications then each key will be deleted individually to ensure that only data owned by this application is purged (leaving external application cache data unchanged). This can result in reduced performance when purging the cache, depending on your server configuration.
+
+If you are running a dedicated cache for this application then the entire cache can safely be flushed without any risk of destroying another application's cache data. This should result in increased performance when purging the cache.
+";
 $string['pluginname'] = 'Memcached';
 $string['prefix'] = 'Prefix key';
 $string['prefix_help'] = 'This can be used to create a "domain" for your item keys allowing you to create multiple memcached stores on a single memcached installation. It cannot be longer than 16 characters in order to ensure key length issues are not encountered.';
@@ -88,3 +95,5 @@ $string['useserialiser'] = 'Use serialiser';
 $string['useserialiser_help'] = 'Specifies the serializer to use for serializing non-scalar values.
 The valid serializers are Memcached::SERIALIZER_PHP or Memcached::SERIALIZER_IGBINARY.
 The latter is supported only when memcached is configured with --enable-memcached-igbinary option and the igbinary extension is loaded.';
+$string['upgrade200recommended'] = 'We recommend you upgrade your Memcached PHP extension to version 2.0.0 or greater.
+The version of the Memcached PHP extension you are currently using does not provide the functionality Moodle uses to ensure a sandboxed cache. Until you upgrade we recommend you do not configure any other applications to use the same Memcached servers as Moodle is configured to use.';
index 5e8250d..9ecb706 100644 (file)
@@ -44,6 +44,12 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class cachestore_memcached extends cache_store implements cache_is_configurable {
+
+    /**
+     * The minimum required version of memcached in order to use this store.
+     */
+    const REQUIRED_VERSION = '2.0.0';
+
     /**
      * The name of the store
      * @var store
@@ -104,6 +110,26 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      */
     protected $setconnections = array();
 
+    /**
+     * The prefix to use on all keys.
+     * @var string
+     */
+    protected $prefix = '';
+
+    /**
+     * True if Memcached::deleteMulti can be used, false otherwise.
+     * This required extension version 2.0.0 or greater.
+     * @var bool
+     */
+    protected $candeletemulti = false;
+
+    /**
+     * True if the memcached server is shared, false otherwise.
+     * This required extension version 2.0.0 or greater.
+     * @var bool
+     */
+    protected $isshared = false;
+
     /**
      * Constructs the store instance.
      *
@@ -171,7 +197,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
 
         $this->options[Memcached::OPT_COMPRESSION] = $compression;
         $this->options[Memcached::OPT_SERIALIZER] = $serialiser;
-        $this->options[Memcached::OPT_PREFIX_KEY] = $prefix;
+        $this->options[Memcached::OPT_PREFIX_KEY] = $this->prefix = (string)$prefix;
         $this->options[Memcached::OPT_HASH] = $hashmethod;
         $this->options[Memcached::OPT_BUFFER_WRITES] = $bufferwrites;
 
@@ -196,6 +222,13 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
             }
         }
 
+        if (isset($configuration['isshared'])) {
+            $this->isshared = $configuration['isshared'];
+        }
+
+        $version = phpversion('memcached');
+        $this->candeletemulti = ($version && version_compare($version, self::REQUIRED_VERSION, '>='));
+
         // Test the connection to the main connection.
         $this->isready = @$this->connection->set("ping", 'ping', 1);
     }
@@ -205,6 +238,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      *
      * Once this has been done the cache is all set to be used.
      *
+     * @throws coding_exception if the instance has already been initialised.
      * @param cache_definition $definition
      */
     public function initialise(cache_definition $definition) {
@@ -238,7 +272,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      * @return bool
      */
     public static function are_requirements_met() {
-        return class_exists('Memcached');
+        return extension_loaded('memcached') && class_exists('Memcached');
     }
 
     /**
@@ -411,6 +445,18 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      */
     protected function delete_many_connection(Memcached $connection, array $keys) {
         $count = 0;
+        if ($this->candeletemulti) {
+            // We can use deleteMulti, this is a bit faster yay!
+            $result = $connection->deleteMulti($keys);
+            foreach ($result as $key => $outcome) {
+                if ($outcome === true) {
+                    $count++;
+                }
+            }
+            return $count;
+        }
+
+        // They are running an older version of the php memcached extension.
         foreach ($keys as $key) {
             if ($connection->delete($key)) {
                 $count++;
@@ -426,18 +472,52 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      */
     public function purge() {
         if ($this->isready) {
+            // Only use delete multi if we have the correct extension installed and if the memcached
+            // server is shared (flushing the cache is quicker otherwise).
+            $candeletemulti = ($this->candeletemulti && $this->isshared);
+
             if ($this->clustered) {
                 foreach ($this->setconnections as $connection) {
-                    $connection->flush();
+                    if ($candeletemulti) {
+                        $keys = self::get_prefixed_keys($connection, $this->prefix);
+                        $connection->deleteMulti($keys);
+                    } else {
+                        // Oh damn, this isn't multi-site safe.
+                        $connection->flush();
+                    }
                 }
+            } else if ($candeletemulti) {
+                $keys = self::get_prefixed_keys($this->connection, $this->prefix);
+                $this->connection->deleteMulti($keys);
             } else {
+                // Oh damn, this isn't multi-site safe.
                 $this->connection->flush();
             }
         }
-
+        // It never fails. Ever.
         return true;
     }
 
+    /**
+     * Returns all of the keys in the given connection that belong to this cache store instance.
+     *
+     * Requires php memcached extension version 2.0.0 or greater.
+     *
+     * @param Memcached $connection
+     * @param string $prefix
+     * @return array
+     */
+    protected static function get_prefixed_keys(Memcached $connection, $prefix) {
+        $keys = array();
+        $start = strlen($prefix);
+        foreach ($connection->getAllKeys() as $key) {
+            if (strpos($key, $prefix) === 0) {
+                $keys[] = substr($key, $start);
+            }
+        }
+        return $keys;
+    }
+
     /**
      * Gets an array of options to use as the serialiser.
      * @return array
@@ -514,6 +594,11 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
             }
         }
 
+        $isshared = false;
+        if (isset($data->isshared)) {
+            $isshared = $data->isshared;
+        }
+
         return array(
             'servers' => $servers,
             'compression' => $data->compression,
@@ -522,7 +607,8 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
             'hash' => $data->hash,
             'bufferwrites' => $data->bufferwrites,
             'clustered' => $clustered,
-            'setservers' => $setservers
+            'setservers' => $setservers,
+            'isshared' => $isshared
         );
     }
 
@@ -566,6 +652,9 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
             }
             $data['setservers'] = join("\n", $servers);
         }
+        if (isset($config['isshared'])) {
+            $data['isshared'] = $config['isshared'];
+        }
         $editform->set_data($data);
     }
 
@@ -585,6 +674,8 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
                 $connection->addServers($this->servers);
             }
         }
+        // We have to flush here to be sure we are completely cleaned up.
+        // Bad for performance but this is incredibly rare.
         @$connection->flush();
         unset($connection);
         unset($this->connection);
index 506e583..6a58844 100644 (file)
@@ -54,6 +54,10 @@ class cachestore_memcached_test extends cachestore_tests {
      * Tests the valid keys to ensure they work.
      */
     public function test_valid_keys() {
+        if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
+            $this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
+        }
+
         $this->resetAfterTest(true);
 
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
@@ -139,16 +143,16 @@ class cachestore_memcached_test extends cachestore_tests {
      * Tests the clustering feature.
      */
     public function test_clustered() {
-        $this->resetAfterTest(true);
-
-        if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
-            $this->markTestSkipped();
+        if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
+            $this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
         }
 
+        $this->resetAfterTest(true);
+
         $testservers = explode("\n", trim(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
 
         if (count($testservers) < 2) {
-            $this->markTestSkipped();
+            $this->markTestSkipped('Could not test clustered memcached, there are not enough test servers defined.');
         }
 
         // Use the first server as our primary.
@@ -270,4 +274,138 @@ class cachestore_memcached_test extends cachestore_tests {
             }
         }
     }
+
+    /**
+     * Tests that memcached cache store doesn't just flush everything and instead deletes only what belongs to it
+     * when it is marked as a shared cache.
+     */
+    public function test_multi_use_compatibility() {
+        if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
+            $this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
+        }
+
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
+        $cachestore = $this->create_test_cache_with_config($definition, array('isshared' => true));
+        $connection = new Memcached(crc32(__METHOD__));
+        $connection->addServers($this->get_servers(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
+        $connection->setOptions(array(
+            Memcached::OPT_COMPRESSION => true,
+            Memcached::OPT_SERIALIZER => Memcached::SERIALIZER_PHP,
+            Memcached::OPT_PREFIX_KEY => 'phpunit_',
+            Memcached::OPT_BUFFER_WRITES => false
+        ));
+
+        // We must flush first to make sure nothing is there.
+        $connection->flush();
+
+        // Test the cachestore.
+        $this->assertFalse($cachestore->get('test'));
+        $this->assertTrue($cachestore->set('test', 'cachestore'));
+        $this->assertSame('cachestore', $cachestore->get('test'));
+
+        // Test the connection.
+        $this->assertFalse($connection->get('test'));
+        $this->assertEquals(Memcached::RES_NOTFOUND, $connection->getResultCode());
+        $this->assertTrue($connection->set('test', 'connection'));
+        $this->assertSame('connection', $connection->get('test'));
+
+        // Test both again and make sure the values are correct.
+        $this->assertSame('cachestore', $cachestore->get('test'));
+        $this->assertSame('connection', $connection->get('test'));
+
+        // Purge the cachestore and check the connection was not purged.
+        $this->assertTrue($cachestore->purge());
+        $this->assertFalse($cachestore->get('test'));
+        $this->assertSame('connection', $connection->get('test'));
+    }
+
+    /**
+     * Tests that memcached cache store flushes entire cache when it is using a dedicated cache.
+     */
+    public function test_dedicated_cache() {
+        if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
+            $this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
+        }
+
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
+        $cachestore = $this->create_test_cache_with_config($definition, array('isshared' => false));
+        $connection = new Memcached(crc32(__METHOD__));
+        $connection->addServers($this->get_servers(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
+        $connection->setOptions(array(
+            Memcached::OPT_COMPRESSION => true,
+            Memcached::OPT_SERIALIZER => Memcached::SERIALIZER_PHP,
+            Memcached::OPT_PREFIX_KEY => 'phpunit_',
+            Memcached::OPT_BUFFER_WRITES => false
+        ));
+
+        // We must flush first to make sure nothing is there.
+        $connection->flush();
+
+        // Test the cachestore.
+        $this->assertFalse($cachestore->get('test'));
+        $this->assertTrue($cachestore->set('test', 'cachestore'));
+        $this->assertSame('cachestore', $cachestore->get('test'));
+
+        // Test the connection.
+        $this->assertFalse($connection->get('test'));
+        $this->assertEquals(Memcached::RES_NOTFOUND, $connection->getResultCode());
+        $this->assertTrue($connection->set('test', 'connection'));
+        $this->assertSame('connection', $connection->get('test'));
+
+        // Test both again and make sure the values are correct.
+        $this->assertSame('cachestore', $cachestore->get('test'));
+        $this->assertSame('connection', $connection->get('test'));
+
+        // Purge the cachestore and check the connection was also purged.
+        $this->assertTrue($cachestore->purge());
+        $this->assertFalse($cachestore->get('test'));
+        $this->assertFalse($connection->get('test'));
+    }
+
+    /**
+     * Given a server string this returns an array of servers.
+     *
+     * @param string $serverstring
+     * @return array
+     */
+    public function get_servers($serverstring) {
+        $servers = array();
+        foreach (explode("\n", $serverstring) as $server) {
+            if (!is_array($server)) {
+                $server = explode(':', $server, 3);
+            }
+            if (!array_key_exists(1, $server)) {
+                $server[1] = 11211;
+                $server[2] = 100;
+            } else if (!array_key_exists(2, $server)) {
+                $server[2] = 100;
+            }
+            $servers[] = $server;
+        }
+        return $servers;
+    }
+
+    /**
+     * Creates a test instance for unit tests.
+     * @param cache_definition $definition
+     * @param array $configuration
+     * @return null|cachestore_memcached
+     */
+    private function create_test_cache_with_config(cache_definition $definition, $configuration = array()) {
+        $class = $this->get_class_name();
+
+        if (!$class::are_requirements_met()) {
+            return null;
+        }
+        if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
+            return null;
+        }
+
+        $configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS);
+
+        $store = new $class('Test memcached', $configuration);
+        $store->initialise($definition);
+
+        return $store;
+    }
 }
index 7902359..e37e3ac 100644 (file)
@@ -53,25 +53,26 @@ $time = optional_param('time', 0, PARAM_INT);
 
 $url = new moodle_url('/calendar/view.php');
 
-if ($courseid != SITEID) {
-    $url->param('course', $courseid);
-}
-
-if ($view !== 'upcoming') {
-    $url->param('view', $view);
-}
-
 // If a day, month and year were passed then convert it to a timestamp. If these were passed
 // then we can assume the day, month and year are passed as Gregorian, as no where in core
 // should we be passing these values rather than the time. This is done for BC.
 if (!empty($day) && !empty($mon) && !empty($year)) {
     if (checkdate($mon, $day, $year)) {
         $time = make_timestamp($year, $mon, $day);
-    } else {
-        $time = usergetmidnight(time());
     }
-} else if (empty($time)) {
-    $time = usergetmidnight(time());
+}
+
+if (empty($time)) {
+    $time = time();
+}
+
+if ($courseid != SITEID) {
+    $url->param('course', $courseid);
+}
+
+if ($view !== 'upcoming') {
+    $time = usergetmidnight($time);
+    $url->param('view', $view);
 }
 
 $url->param('time', $time);
index 9999c40..2a76ecd 100644 (file)
@@ -49,6 +49,10 @@ class api {
     /**
      * Returns whether competencies are enabled.
      *
+     * This method should never do more than checking the config setting, the reason
+     * being that some other code could be checking the config value directly
+     * to avoid having to load this entire file into memory.
+     *
      * @return boolean True when enabled.
      */
     public static function is_enabled() {
@@ -1606,12 +1610,11 @@ class api {
         $record->courseid = $courseid;
         $record->competencyid = $competencyid;
 
-        $competency = new competency($competencyid);
         $coursecompetency = new course_competency();
-        $exists = $coursecompetency->get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
+        $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
         if ($exists) {
             // Delete all course_module_competencies for this competency in this course.
-            $cmcs = course_module_competency::list_course_module_competencies($competencyid, $courseid);
+            $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
             foreach ($cmcs as $cmc) {
                 $cmc->delete();
             }
index de6363e..2065ba2 100644 (file)
@@ -298,4 +298,33 @@ class course_module_competency extends persistent {
         return $instances;
     }
 
+    /**
+     * List the relationship objects for a competency in a course.
+     *
+     * @param int $competencyid The competency ID.
+     * @param int $courseid The course ID.
+     * @return course_module_competency[]
+     */
+    public static function get_records_by_competencyid_in_course($competencyid, $courseid) {
+        global $DB;
+
+        $sql = 'SELECT cmc.*
+                  FROM {' . self::TABLE . '} cmc
+                  JOIN {course_modules} cm
+                    ON cm.course = ?
+                   AND cmc.cmid = cm.id
+                 WHERE cmc.competencyid = ?
+              ORDER BY cmc.sortorder ASC';
+        $params = array($courseid, $competencyid);
+
+        $results = $DB->get_recordset_sql($sql, $params);
+        $instances = array();
+        foreach ($results as $result) {
+            $instances[$result->id] = new course_module_competency(0, $result);
+        }
+        $results->close();
+
+        return $instances;
+    }
+
 }
index 6771182..58b548e 100644 (file)
@@ -138,24 +138,25 @@ class related_competency extends persistent {
     public static function get_related_competencies($competencyid) {
         global $DB;
 
-        $sql = "(SELECT c.*, " . $DB->sql_concat('rc.relatedcompetencyid', "'_'", 'rc.competencyid') . " AS rid
+        $fields = competency::get_sql_fields('c', 'c_');
+        $sql = "(SELECT $fields, " . $DB->sql_concat('rc.relatedcompetencyid', "'_'", 'rc.competencyid') . " AS rid
                    FROM {" . self::TABLE . "} rc
                    JOIN {" . competency::TABLE . "} c
                      ON c.id = rc.relatedcompetencyid
                   WHERE rc.competencyid = :cid)
               UNION ALL
-                (SELECT c.*, " . $DB->sql_concat('rc.competencyid', "'_'", 'rc.relatedcompetencyid') . " AS rid
+                (SELECT $fields, " . $DB->sql_concat('rc.competencyid', "'_'", 'rc.relatedcompetencyid') . " AS rid
                    FROM {" . self::TABLE . "} rc
                    JOIN {" . competency::TABLE . "} c
                      ON c.id = rc.competencyid
                   WHERE rc.relatedcompetencyid = :cid2)
-               ORDER BY path, sortorder ASC";
+               ORDER BY c_path ASC, c_sortorder ASC";
 
         $competencies = array();
         $records = $DB->get_recordset_sql($sql, array('cid' => $competencyid, 'cid2' => $competencyid));
         foreach ($records as $record) {
             unset($record->rid);
-            $competencies[$record->id] = new competency(null, $record);
+            $competencies[$record->c_id] = new competency(null, competency::extract_record($record, 'c_'));
         }
         $records->close();
 
index 8b24882..41019e2 100644 (file)
@@ -107,6 +107,8 @@ class template_competency extends persistent {
             $params[] = 1;
         }
 
+        $sql .= ' ORDER BY tpl.id ASC';
+
         $results = $DB->get_records_sql($sql, $params);
 
         $instances = array();
@@ -199,11 +201,10 @@ class template_competency extends persistent {
                   FROM {' . competency::TABLE . '} comp
                   JOIN {' . self::TABLE . '} tplcomp
                     ON tplcomp.competencyid = comp.id
-                 WHERE tplcomp.templateid = ?';
+                 WHERE tplcomp.templateid = ?
+              ORDER BY tplcomp.sortorder ASC';
         $params = array($templateid);
 
-        $sql .= 'ORDER BY tplcomp.sortorder ASC';
-
         $results = $DB->get_records_sql($sql, $params);
 
         $instances = array();
index 93e2c3c..e731ca6 100644 (file)
@@ -263,24 +263,27 @@ class user_competency_course extends persistent {
     public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 0) {
         global $DB;
 
-        $fields = competency::get_sql_fields('c');
+        $fields = competency::get_sql_fields('c', 'c_');
         $params = array('courseid' => $courseid);
-        $sql = 'SELECT ' . $fields . ', SUM(COALESCE(ucc.proficiency, 0)) AS timesproficient ' .
-                ' FROM {' . competency::TABLE . '} c
-                  JOIN {' . course_competency::TABLE . '} cc
-                    ON c.id = cc.competencyid
-                  LEFT JOIN {' . self::TABLE . '} ucc
-                    ON ucc.competencyid = c.id AND ucc.courseid = cc.courseid
-                 WHERE cc.courseid = :courseid
-                GROUP BY c.id
-                ORDER BY timesproficient ASC, c.id DESC';
+        $sql = 'SELECT ' . $fields . '
+                  FROM (SELECT cc.competencyid, SUM(COALESCE(ucc.proficiency, 0)) AS timesproficient
+                          FROM {' . course_competency::TABLE . '} cc
+                     LEFT JOIN {' . self::TABLE . '} ucc
+                                ON ucc.competencyid = cc.competencyid
+                               AND ucc.courseid = cc.courseid
+                         WHERE cc.courseid = :courseid
+                      GROUP BY cc.competencyid
+                     ) p
+                  JOIN {' . competency::TABLE . '} c
+                    ON c.id = p.competencyid
+              ORDER BY p.timesproficient ASC, c.id DESC';
 
         $results = $DB->get_records_sql($sql, $params, $skip, $limit);
         $a = $DB->get_records_sql('SELECT * from {' . self::TABLE . '}');
 
         $comps = array();
         foreach ($results as $r) {
-            $c = competency::extract_record($r);
+            $c = competency::extract_record($r, 'c_');
             $comps[] = new competency(0, $c);
         }
         return $comps;
index 1bbe937..81b7791 100644 (file)
@@ -340,24 +340,26 @@ class user_competency_plan extends persistent {
     public static function get_least_proficient_competencies_for_template($templateid, $skip = 0, $limit = 0) {
         global $DB;
 
-        $fields = competency::get_sql_fields('c');
+        $fields = competency::get_sql_fields('c', 'c_');
         $params = array('templateid' => $templateid, 'notproficient' => false);
-        $sql = 'SELECT ' . $fields . ', COUNT(c.id) AS timesnotproficient ' .
-                ' FROM {' . self::TABLE . '} ucp
-                  JOIN {' . plan::TABLE . '} p
-                    ON ucp.planid = p.id
+        $sql = 'SELECT ' . $fields . '
+                  FROM (SELECT ucp.competencyid, COUNT(ucp.competencyid) AS timesnotproficient
+                          FROM {' . self::TABLE . '} ucp
+                          JOIN {' . plan::TABLE . '} p
+                               ON p.id = ucp.planid
+                         WHERE p.templateid = :templateid
+                               AND (ucp.proficiency = :notproficient OR ucp.proficiency IS NULL)
+                      GROUP BY ucp.competencyid
+                     ) p
                   JOIN {' . competency::TABLE . '} c
-                    ON ucp.competencyid = c.id
-                 WHERE p.templateid = :templateid
-                    AND (ucp.proficiency = :notproficient OR ucp.proficiency IS NULL)
-                GROUP BY c.id
-                ORDER BY timesnotproficient DESC';
+                    ON c.id = p.competencyid
+              ORDER BY p.timesnotproficient DESC, c.id ASC';
 
         $results = $DB->get_records_sql($sql, $params, $skip, $limit);
 
         $comps = array();
         foreach ($results as $r) {
-            $c = competency::extract_record($r);
+            $c = competency::extract_record($r, 'c_');
             $comps[] = new competency(0, $c);
         }
         return $comps;
index b424d06..0c3272b 100644 (file)
@@ -122,7 +122,8 @@ class user_evidence_competency extends persistent {
                   JOIN {" . user_evidence::TABLE . "} ue
                     ON uec.userevidenceid = ue.id
                    AND uc.userid = ue.userid
-                   AND ue.id = ?";
+                   AND ue.id = ?
+              ORDER BY uc.id ASC";
 
         $usercompetencies = array();
         $records = $DB->get_recordset_sql($sql, array($userevidenceid));
index c3b4b05..e900774 100644 (file)
@@ -40,7 +40,7 @@ use core_competency\user_evidence;
 function core_competency_comment_add($comment, $params) {
     global $USER;
 
-    if (!api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return;
     }
 
@@ -215,7 +215,7 @@ function core_competency_comment_add($comment, $params) {
  * @return array
  */
 function core_competency_comment_permissions($params) {
-    if (!api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return array('post' => false, 'view' => false);
     }
 
@@ -241,7 +241,7 @@ function core_competency_comment_permissions($params) {
  * @return bool
  */
 function core_competency_comment_validate($params) {
-    if (!api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return false;
     }
 
@@ -274,7 +274,7 @@ function core_competency_comment_validate($params) {
 function core_competency_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
     global $CFG;
 
-    if (!api::is_enabled()) {
+    if (!get_config('core_competency', 'enabled')) {
         return false;
     }
 
index 30d8c15..262bcf9 100644 (file)
@@ -242,6 +242,10 @@ $CFG->admin = 'admin';
 //      $CFG->session_handler_class = '\core\session\file';
 //      $CFG->session_file_save_path = $CFG->dataroot.'/sessions';
 //
+//   Redis session handler (requires redis server and redis extension):
+//      $CFG->session_handler_class = '\core\session\redis';
+//      $CFG->session_redis_save_path = 'tcp://127.0.0.1'
+//
 //   Memcached session handler (requires memcached server and extension):
 //      $CFG->session_handler_class = '\core\session\memcached';
 //      $CFG->session_memcached_save_path = '127.0.0.1:11211';
diff --git a/dataformat/csv/classes/writer.php b/dataformat/csv/classes/writer.php
new file mode 100644 (file)
index 0000000..8ccea00
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * CSV data format writer
+ *
+ * @package    dataformat_csv
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace dataformat_csv;
+
+require_once("$CFG->libdir/spout/src/Spout/Autoloader/autoload.php");
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * CSV data format writer
+ *
+ * @package    dataformat_csv
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class writer extends \core\dataformat\spout_base {
+
+    /** @var $mimetype */
+    protected $mimetype = "text/csv";
+
+    /** @var $extension */
+    protected $extension = ".csv";
+
+    /** @var $spouttype */
+    protected $spouttype = \Box\Spout\Common\Type::CSV;
+
+}
+
diff --git a/dataformat/csv/lang/en/dataformat_csv.php b/dataformat/csv/lang/en/dataformat_csv.php
new file mode 100644 (file)
index 0000000..033e32d
--- /dev/null
@@ -0,0 +1,27 @@
+<?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/>.
+
+/**
+ * CSV dataformat lang strings.
+ *
+ * @package    dataformat_csv
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['dataformat'] = 'Comma separated values (.csv)';
+$string['shortname'] = 'CSV';
+
diff --git a/dataformat/csv/version.php b/dataformat/csv/version.php
new file mode 100644 (file)
index 0000000..fad7203
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Data activity filter version information
+ *
+ * @package    dataformat_csv
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2016031700;
+$plugin->requires  = 2016031700;  // Requires this Moodle version.
+$plugin->component = 'dataformat_csv';
+
diff --git a/dataformat/excel/classes/writer.php b/dataformat/excel/classes/writer.php
new file mode 100644 (file)
index 0000000..0783e9e
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Excel data format writer
+ *
+ * @package    dataformat_excel
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace dataformat_excel;
+
+require_once("$CFG->libdir/spout/src/Spout/Autoloader/autoload.php");
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Excel data format writer
+ *
+ * @package    dataformat_excel
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class writer extends \core\dataformat\spout_base {
+
+    /** @var $mimetype */
+    protected $mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+
+    /** @var $extension */
+    protected $extension = ".xlsx";
+
+    /** @var $spouttype */
+    protected $spouttype = \Box\Spout\Common\Type::XLSX;
+
+    /**
+     * Set the title of the worksheet inside a spreadsheet
+     *
+     * For some formats this will be ignored.
+     *
+     * @param string $title
+     */
+    public function set_sheettitle($title) {
+        if (!$title) {
+            return;
+        }
+
+        // Replace any characters in the name that Excel cannot cope with.
+        $title = strtr(trim($title, "'"), '[]*/\?:', '       ');
+        // Shorten the title if necessary.
+        $title = \core_text::substr($title, 0, 31);
+        // After the substr, we might now have a single quote on the end.
+        $title = trim($title, "'");
+
+        $this->sheettitle = $title;
+    }
+}
+
diff --git a/dataformat/excel/lang/en/dataformat_excel.php b/dataformat/excel/lang/en/dataformat_excel.php
new file mode 100644 (file)
index 0000000..b286875
--- /dev/null
@@ -0,0 +1,27 @@
+<?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/>.
+
+/**
+ * Excel dataformat lang strings.
+ *
+ * @package    dataformat_excel
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['dataformat'] = 'Microsoft Excel (.xlsx)';
+$string['shortname'] = 'Excel';
+
diff --git a/dataformat/excel/version.php b/dataformat/excel/version.php
new file mode 100644 (file)
index 0000000..b6b5ffc
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Data activity filter version information
+ *
+ * @package    dataformat_excel
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2016031700;
+$plugin->requires  = 2016031700;  // Requires this Moodle version.
+$plugin->component = 'dataformat_excel';
+
diff --git a/dataformat/html/classes/writer.php b/dataformat/html/classes/writer.php
new file mode 100644 (file)
index 0000000..90ca368
--- /dev/null
@@ -0,0 +1,111 @@
+<?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/>.
+
+/**
+ * html data format writer
+ *
+ * @package    dataformat_html
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace dataformat_html;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * html data format writer
+ *
+ * @package    dataformat_html
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class writer extends \core\dataformat\base {
+
+    /** @var $mimetype */
+    public $mimetype = "text/html";
+
+    /** @var $extension */
+    public $extension = ".html";
+
+    /**
+     * Write the start of the format
+     *
+     * @param array $columns
+     */
+    public function write_header($columns) {
+        echo "<!DOCTYPE html><html>";
+        echo \html_writer::tag('title', $this->filename);
+        echo "<style>
+html, body {
+    margin: 0;
+    padding: 0;
+    font-family: sans-serif;
+    font-size: 13px;
+    background: #eee;
+}
+th {
+    border: solid 1px #999;
+    background: #eee;
+}
+td {
+    border: solid 1px #999;
+    background: #fff;
+}
+tr:hover td {
+    background: #eef;
+}
+table {
+    border-collapse: collapse;
+    border-spacing: 0pt;
+    width: 80%;
+    margin: auto;
+}
+</style>
+<body>
+<table border=1 cellspacing=0 cellpadding=3>
+";
+        echo \html_writer::start_tag('tr');
+        foreach ($columns as $k => $v) {
+            echo \html_writer::tag('th', $v);
+        }
+        echo \html_writer::end_tag('tr');
+    }
+
+    /**
+     * Write a single record
+     *
+     * @param array $record
+     * @param int $rownum
+     */
+    public function write_record($record, $rownum) {
+        echo \html_writer::start_tag('tr');
+        foreach ($record as $cell) {
+            echo \html_writer::tag('td', $cell);
+        }
+        echo \html_writer::end_tag('tr');
+    }
+
+    /**
+     * Write the end of the format
+     *
+     * @param array $columns
+     */
+    public function write_footer($columns) {
+        echo "</table></body></html>";
+    }
+
+}
diff --git a/dataformat/html/lang/en/dataformat_html.php b/dataformat/html/lang/en/dataformat_html.php
new file mode 100644 (file)
index 0000000..df87409
--- /dev/null
@@ -0,0 +1,27 @@
+<?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/>.
+
+/**
+ * html dataformat lang strings.
+ *
+ * @package    dataformat_html
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['dataformat'] = 'HTML table';
+$string['shortname'] = 'HTML';
+
diff --git a/dataformat/html/version.php b/dataformat/html/version.php
new file mode 100644 (file)
index 0000000..ac903a1
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * Data activity filter version information
+ *
+ * @package    dataformat_html
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2016031700;
+$plugin->requires  = 2016031700;  // Requires this Moodle version.
+$plugin->component = 'dataformat_html';
diff --git a/dataformat/json/classes/writer.php b/dataformat/json/classes/writer.php
new file mode 100644 (file)
index 0000000..84f3cc6
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * JSON data format writer
+ *
+ * @package    dataformat_json
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace dataformat_json;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * JSON data format writer
+ *
+ * @package    dataformat_json
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class writer extends \core\dataformat\base {
+
+    /** @var $mimetype */
+    public $mimetype = "application/json";
+
+    /** @var $extension */
+    public $extension = ".json";
+
+    /**
+     * Write the start of the format
+     *
+     * @param array $columns
+     */
+    public function write_header($columns) {
+        echo "[";
+    }
+
+    /**
+     * Write a single record
+     *
+     * @param array $record
+     * @param int $rownum
+     */
+    public function write_record($record, $rownum) {
+        if ($rownum) {
+            echo ",";
+        }
+        echo json_encode($record);
+    }
+
+    /**
+     * Write the end of the format
+     *
+     * @param array $columns
+     */
+    public function write_footer($columns) {
+        echo "]";
+    }
+
+}
diff --git a/dataformat/json/lang/en/dataformat_json.php b/dataformat/json/lang/en/dataformat_json.php
new file mode 100644 (file)
index 0000000..60e77bb
--- /dev/null
@@ -0,0 +1,27 @@
+<?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/>.
+
+/**
+ * JSON dataformat lang strings.
+ *
+ * @package    dataformat_json
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['dataformat'] = 'Javascript Object Notation (.json)';
+$string['shortname'] = 'JSON';
+
diff --git a/dataformat/json/version.php b/dataformat/json/version.php
new file mode 100644 (file)
index 0000000..41ef909
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * Data activity filter version information
+ *
+ * @package    dataformat_json
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2016031700;
+$plugin->requires  = 2016031700;  // Requires this Moodle version.
+$plugin->component = 'dataformat_json';
diff --git a/dataformat/ods/classes/writer.php b/dataformat/ods/classes/writer.php
new file mode 100644 (file)
index 0000000..8f28228
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * ODS data format writer
+ *
+ * @package    dataformat_ods
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace dataformat_ods;
+
+require_once("$CFG->libdir/spout/src/Spout/Autoloader/autoload.php");
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * ODS data format writer
+ *
+ * @package    dataformat_ods
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class writer extends \core\dataformat\spout_base {
+
+    /** @var $mimetype */
+    protected $mimetype = "application/vnd.oasis.opendocument.spreadsheet";
+
+    /** @var $extension */
+    protected $extension = ".ods";
+
+    /** @var $spouttype */
+    protected $spouttype = \Box\Spout\Common\Type::ODS;
+
+    /**
+     * Set the title of the worksheet inside a spreadsheet
+     *
+     * For some formats this will be ignored.
+     *
+     * @param string $title
+     */
+    public function set_sheettitle($title) {
+        if (!$title) {
+            return;
+        }
+
+        // Replace any characters in the name that ODS cannot cope with.
+        $title = strtr(trim($title, "'"), '[]*/\?:', '       ');
+        // Shorten the title if necessary.
+        $title = \core_text::substr($title, 0, 31);
+        // After the substr, we might now have a single quote on the end.
+        $title = trim($title, "'");
+
+        $this->sheettitle = $title;
+    }
+}
+
diff --git a/dataformat/ods/lang/en/dataformat_ods.php b/dataformat/ods/lang/en/dataformat_ods.php
new file mode 100644 (file)
index 0000000..9c1c44e
--- /dev/null
@@ -0,0 +1,27 @@
+<?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/>.
+
+/**
+ * ODS dataformat lang strings.
+ *
+ * @package    dataformat_ods
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['dataformat'] = 'OpenDocument (.ods)';
+$string['shortname'] = 'OpenDoc';
+
diff --git a/dataformat/ods/version.php b/dataformat/ods/version.php
new file mode 100644 (file)
index 0000000..41452d6
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Data activity filter version information
+ *
+ * @package    dataformat_ods
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2016031700;
+$plugin->requires  = 2016031700;  // Requires this Moodle version.
+$plugin->component = 'dataformat_ods';
+
diff --git a/dataformat/upgrade.txt b/dataformat/upgrade.txt
new file mode 100644 (file)
index 0000000..bfd29d1
--- /dev/null
@@ -0,0 +1,7 @@
+This files describes API changes in /dataformat/ download system,
+information provided here is intended especially for developers.
+
+=== 3.1 ===
+* Added new plugin system with low memory support for csv, ods, xls and json
+
+
index d1ae180..a9901ca 100644 (file)
@@ -73,7 +73,7 @@ Structure of the user enroller panel
 .user-enroller-panel .uep-footer {padding:3px;background-color:#ddd;text-align:center;}
 .user-enroller-panel .uep-search {margin:3px;}
 .user-enroller-panel .uep-search label {padding-right:8px;}
-.user-enroller-panel .uep-search input {width:50%;}
+.user-enroller-panel .uep-search input {width:50%; margin: 0 0 10px 5px}
 .user-enroller-panel .uep-search input.uep-search-btn {width:20%;}
 .user-enroller-panel .uep-searchoptions {margin:3px;cursor:pointer;}
 .user-enroller-panel .uep-controls select {margin-left:1em;margin-bottom:0;}
index 3c8517f..b4132cc 100644 (file)
@@ -114,6 +114,10 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                                         .append(create('<select><option value="0" selected="selected">'+M.util.get_string('unlimitedduration', 'enrol')+'</option></select>')))
                                 )
                             )
+                            .append(create('<div class="'+CSS.SEARCH+'"><label for="enrolusersearch" class="accesshide">'+M.util.get_string('usersearch', 'enrol')+'</label></div>')
+                                .append(create('<input type="text" id="enrolusersearch" value="" />'))
+                                .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+'" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
+                            )
                         )
                         .append(create('<div class="'+CSS.AJAXCONTENT+'"></div>'))
                         .append(create('<div class="'+CSS.LIGHTBOX+' '+CSS.HIDDEN+'"></div>')
@@ -121,10 +125,6 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                                 .setAttribute('src', M.util.image_url('i/loading', 'moodle')))
                             .setStyle('opacity', 0.5)))
                     .append(create('<div class="'+CSS.FOOTER+'"></div>')
-                        .append(create('<div class="'+CSS.SEARCH+'"><label for="enrolusersearch" class="accesshide">'+M.util.get_string('usersearch', 'enrol')+'</label></div>')
-                            .append(create('<input type="text" id="enrolusersearch" value="" />'))
-                                .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+'" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
-                        )
                         .append(create('<div class="'+CSS.CLOSEBTN+'"></div>')
                             .append(create('<input type="button" value="'+M.util.get_string('finishenrollingusers', 'enrol')+'" />'))
                         )
index 9455d53..8607a81 100644 (file)
@@ -1146,6 +1146,7 @@ $string['useblogassociations'] = 'Enable associations';
 $string['useexternalyui'] = 'Use online YUI libraries';
 $string['user'] = 'User';
 $string['userbulk'] = 'Bulk user actions';
+$string['userbulkdownload'] = 'Export users as';
 $string['userlist'] = 'Browse list of users';
 $string['userdefaultpreferences'] = 'User default preferences';
 $string['userpreference'] = 'User preference';
index 7da297a..159bc3b 100644 (file)
@@ -30,3 +30,9 @@ settypeofficial,core_tag
 filetoolarge,core
 maxbytesforfile,core
 maxbytes,core_error
+downloadcsv,core_table
+downloadexcel,core_table
+downloadods,core_table
+downloadoptions,core_table
+downloadtsv,core_table
+downloadxhtml,core_table
index 092ec78..5400fd7 100644 (file)
@@ -352,6 +352,7 @@ $string['invalidurl'] = 'Invalid URL';
 $string['invaliduser'] = 'Invalid user';
 $string['invaliduserid'] = 'Invalid user id';
 $string['invaliduserfield'] = 'Invalid user field: {$a}';
+$string['invaliduserdata'] = 'Invalid user data: {$a}';
 $string['invalidusername'] = 'The given username contains invalid characters';
 $string['invalidxmlfile'] = '"{$a}" is not a valid XML file';
 $string['iplookupfailed'] = 'Cannot find geo information about this IP address {$a}';
index d9cac66..d28d5e4 100644 (file)
@@ -431,6 +431,7 @@ $string['databaseupgradeblocks'] = 'Blocks version is now {$a}';
 $string['databaseupgradegroups'] = 'Groups version is now {$a}';
 $string['databaseupgradelocal'] = 'Local database customisations version is now {$a}';
 $string['databaseupgrades'] = 'Upgrading database';
+$string['dataformats'] = 'Data formats';
 $string['date'] = 'Date';
 $string['datechanged'] = 'Date changed';
 $string['datemostrecentfirst'] = 'Date - most recent first';
@@ -557,7 +558,10 @@ $string['edituser'] = 'Edit user accounts';
 $string['edulevelother'] = 'Other';
 $string['edulevelteacher'] = 'Teaching';
 $string['edulevelparticipating'] = 'Participating';
-$string['edulevel'] = 'Educational level';
+$string['edulevel'] = 'All events';
+$string['edulevel_help'] = '* Teaching - actions performed by a teacher, e.g. updating a resource
+* Participating - actions performed by a student, e.g. posting in a forum
+* Other - actions performed by a user with a role other than teacher or student';
 $string['email'] = 'Email address';
 $string['emailalreadysent'] = 'A password reset email has already been sent. Please check your email.';
 $string['emailactive'] = 'Email activated';
@@ -1088,6 +1092,7 @@ $string['makethismyhome'] = 'Make this my default home page';
 $string['manageblocks'] = 'Blocks';
 $string['managecategorythis'] = 'Manage this category';
 $string['managecourses'] = 'Manage courses';
+$string['managedataformats'] = 'Manage data formats';
 $string['managedatabase'] = 'Database';
 $string['manageeditorfiles'] = 'Manage files used by editor';
 $string['managefilters'] = 'Filters';
index b3d0a5d..ff64fd3 100644 (file)
@@ -121,6 +121,8 @@ $string['type_calendartype'] = 'Calendar type';
 $string['type_calendartype_plural'] = 'Calendar types';
 $string['type_coursereport'] = 'Course report';
 $string['type_coursereport_plural'] = 'Course reports';
+$string['type_dataformat'] = 'Data format';
+$string['type_dataformat_plural'] = 'Data formats';
 $string['type_editor'] = 'Editor';
 $string['type_editor_plural'] = 'Editors';
 $string['type_enrol'] = 'Enrolment method';
index 69a0a8e..8ab01a3 100644 (file)
@@ -22,7 +22,9 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['downloadas'] = 'Download table data as {$a->formatsmenu} {$a->downloadbutton}';
+$string['downloadas'] = 'Download table data as';
+
+// Deprecated since Moodle 3.1.
 $string['downloadcsv'] = 'Comma separated values text file';
 $string['downloadexcel'] = 'Excel spreadsheet';
 $string['downloadods'] = 'OpenDocument spreadsheet';
index 3df2cc3..c12e545 100644 (file)
@@ -6916,6 +6916,165 @@ class admin_setting_manageformats extends admin_setting {
     }
 }
 
+/**
+ * Data formats manager. Allow reorder and to enable/disable data formats and jump to settings
+ *
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_managedataformats extends admin_setting {
+
+    /**
+     * Calls parent::__construct with specific arguments
+     */
+    public function __construct() {
+        $this->nosave = true;
+        parent::__construct('managedataformats', new lang_string('managedataformats'), '', '');
+    }
+
+    /**
+     * Always returns true
+     *
+     * @return true
+     */
+    public function get_setting() {
+        return true;
+    }
+
+    /**
+     * Always returns true
+     *
+     * @return true
+     */
+    public function get_defaultsetting() {
+        return true;
+    }
+
+    /**
+     * Always returns '' and doesn't write anything
+     *
+     * @param mixed $data string or array, must not be NULL
+     * @return string Always returns ''
+     */
+    public function write_setting($data) {
+        // Do not write any setting.
+        return '';
+    }
+
+    /**
+     * Search to find if Query is related to format plugin
+     *
+     * @param string $query The string to search for
+     * @return bool true for related false for not
+     */
+    public function is_related($query) {
+        if (parent::is_related($query)) {
+            return true;
+        }
+        $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
+        foreach ($formats as $format) {
+            if (strpos($format->component, $query) !== false ||
+                    strpos(core_text::strtolower($format->displayname), $query) !== false) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return XHTML to display control
+     *
+     * @param mixed $data Unused
+     * @param string $query
+     * @return string highlight
+     */
+    public function output_html($data, $query='') {
+        global $CFG, $OUTPUT;
+        $return = '';
+
+        $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
+
+        $txt = get_strings(array('settings', 'name', 'enable', 'disable', 'up', 'down', 'default'));
+        $txt->uninstall = get_string('uninstallplugin', 'core_admin');
+        $txt->updown = "$txt->up/$txt->down";
+
+        $table = new html_table();
+        $table->head  = array($txt->name, $txt->enable, $txt->updown, $txt->uninstall, $txt->settings);
+        $table->align = array('left', 'center', 'center', 'center', 'center');
+        $table->attributes['class'] = 'manageformattable generaltable admintable';
+        $table->data  = array();
+
+        $cnt = 0;
+        $spacer = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'iconsmall'));
+        $totalenabled = 0;
+        foreach ($formats as $format) {
+            if ($format->is_enabled() && $format->is_installed_and_upgraded()) {
+                $totalenabled++;
+            }
+        }
+        foreach ($formats as $format) {
+            $status = $format->get_status();
+            $url = new moodle_url('/admin/dataformats.php',
+                    array('sesskey' => sesskey(), 'name' => $format->name));
+
+            $class = '';
+            if ($format->is_enabled()) {
+                $strformatname = $format->displayname;
+                if ($totalenabled == 1&& $format->is_enabled()) {
+                    $hideshow = '';
+                } else {
+                    $hideshow = html_writer::link($url->out(false, array('action' => 'disable')),
+                        $OUTPUT->pix_icon('t/hide', $txt->disable, 'moodle', array('class' => 'iconsmall')));
+                }
+            } else {
+                $class = 'dimmed_text';
+                $strformatname = $format->displayname;
+                $hideshow = html_writer::link($url->out(false, array('action' => 'enable')),
+                    $OUTPUT->pix_icon('t/show', $txt->enable, 'moodle', array('class' => 'iconsmall')));
+            }
+
+            $updown = '';
+            if ($cnt) {
+                $updown .= html_writer::link($url->out(false, array('action' => 'up')),
+                    $OUTPUT->pix_icon('t/up', $txt->up, 'moodle', array('class' => 'iconsmall'))). '';
+            } else {
+                $updown .= $spacer;
+            }
+            if ($cnt < count($formats) - 1) {
+                $updown .= '&nbsp;'.html_writer::link($url->out(false, array('action' => 'down')),
+                    $OUTPUT->pix_icon('t/down', $txt->down, 'moodle', array('class' => 'iconsmall')));
+            } else {
+                $updown .= $spacer;
+            }
+
+            $uninstall = '';
+            if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) {
+                $uninstall = get_string('status_missing', 'core_plugin');
+            } else if ($status === core_plugin_manager::PLUGIN_STATUS_NEW) {
+                $uninstall = get_string('status_new', 'core_plugin');
+            } else if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url('dataformat_'.$format->name, 'manage')) {
+                if ($totalenabled != 1 || !$format->is_enabled()) {
+                    $uninstall = html_writer::link($uninstallurl, $txt->uninstall);
+                }
+            }
+
+            $settings = '';
+            if ($format->get_settings_url()) {
+                $settings = html_writer::link($format->get_settings_url(), $txt->settings);
+            }
+
+            $row = new html_table_row(array($strformatname, $hideshow, $updown, $uninstall, $settings));
+            if ($class) {
+                $row->attributes['class'] = $class;
+            }
+            $table->data[] = $row;
+            $cnt++;
+        }
+        $return .= html_writer::table($table);
+        return highlight($query, $return);
+    }
+}
+
 /**
  * Special class for filter administration.
  *
index 7c372c6..6c8b5de 100644 (file)
@@ -804,6 +804,19 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         }
     }
 
+    /**
+     * Converts HTML tags to line breaks to display the info in CLI
+     *
+     * @param string $html
+     * @return string
+     */
+    protected function get_debug_text($html) {
+
+        // Replacing HTML tags for new lines and keeping only the text.
+        $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html);
+        return preg_replace("/(\n)+/s", "\n", $notags);
+    }
+
     /**
      * Helper function to execute api in a given context.
      *
index f740f22..754e44a 100644 (file)
@@ -431,6 +431,7 @@ $cache = '.var_export($cache, true).';
             'filter'        => $CFG->dirroot.'/filter',
             'editor'        => $CFG->dirroot.'/lib/editor',
             'format'        => $CFG->dirroot.'/course/format',
+            'dataformat'    => $CFG->dirroot.'/dataformat',
             'profilefield'  => $CFG->dirroot.'/user/profile/field',
             'report'        => $CFG->dirroot.'/report',
             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
diff --git a/lib/classes/dataformat/base.php b/lib/classes/dataformat/base.php
new file mode 100644 (file)
index 0000000..d9fe32d
--- /dev/null
@@ -0,0 +1,126 @@
+<?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/>.
+
+/**
+ * Base class for dataformat.
+ *
+ * @package    core
+ * @subpackage dataformat
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\dataformat;
+
+/**
+ * Base class for dataformat.
+ *
+ * @package    core
+ * @subpackage dataformat
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base {
+
+    /** @var $mimetype */
+    protected $mimetype = "text/plain";
+
+    /** @var $extension */
+    protected $extension = ".txt";
+
+    /** @var $filename */
+    protected $filename = '';
+
+    /**
+     * Get the file extension
+     *
+     * @return string file extension
+     */
+    public function get_extension() {
+        return $this->extension;
+    }
+
+    /**
+     * Set download filename base
+     *
+     * @param string $filename
+     */
+    public function set_filename($filename) {
+        $this->filename = $filename;
+    }
+
+    /**
+     * Set the title of the worksheet inside a spreadsheet
+     *
+     * For some formats this will be ignored.
+     *
+     * @param string $title
+     */
+    public function set_sheettitle($title) {
+    }
+
+    /**
+     * Output file headers to initialise the download of the file.
+     */
+    public function send_http_headers() {
+        global $CFG;
+
+        if (defined('BEHAT_SITE_RUNNING')) {
+            // For text based formats - we cannot test the output with behat if we force a file download.
+            return;
+        }
+        if (is_https()) {
+            // HTTPS sites - watch out for IE! KB812935 and KB316431.
+            header('Cache-Control: max-age=10');
+            header('Pragma: ');
+        } else {
+            // Normal http - prevent caching at all cost.
+            header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
+            header('Pragma: no-cache');
+        }
+        header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
+        header("Content-Type: $this->mimetype\n");
+        $filename = $this->filename . $this->get_extension();
+        header("Content-Disposition: attachment; filename=\"$filename\"");
+    }
+
+    /**
+     * Write the start of the format
+     *
+     * @param array $columns
+     */
+    public function write_header($columns) {
+        // Override me if needed.
+    }
+
+    /**
+     * Write a single record
+     *
+     * @param array $record
+     * @param int $rownum
+     */
+    abstract public function write_record($record, $rownum);
+
+    /**
+     * Write the end of the format
+     *
+     * @param array $columns
+     */
+    public function write_footer($columns) {
+        // Override me if needed.
+    }
+
+}
diff --git a/lib/classes/dataformat/spout_base.php b/lib/classes/dataformat/spout_base.php
new file mode 100644 (file)
index 0000000..29c8ed4
--- /dev/null
@@ -0,0 +1,103 @@
+<?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/>.
+
+/**
+ * Common Spout class for dataformat.
+ *
+ * @package    core
+ * @subpackage dataformat
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\dataformat;
+
+/**
+ * Common Spout class for dataformat.
+ *
+ * @package    core
+ * @subpackage dataformat
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class spout_base extends \core\dataformat\base {
+
+    /** @var $spouttype */
+    protected $spouttype = '';
+
+    /** @var $writer */
+    protected $writer;
+
+    /** @var $sheettitle */
+    protected $sheettitle;
+
+    /**
+     * Output file headers to initialise the download of the file.
+     */
+    public function send_http_headers() {
+        $this->writer = \Box\Spout\Writer\WriterFactory::create($this->spouttype);
+        $filename = $this->filename . $this->get_extension();
+        $this->writer->openToBrowser($filename);
+        if ($this->sheettitle) {
+            $sheet = $this->writer->getCurrentSheet();
+            $sheet->setName($this->sheettitle);
+        }
+    }
+
+    /**
+     * Set the title of the worksheet inside a spreadsheet
+     *
+     * For some formats this will be ignored.
+     *
+     * @param string $title
+     */
+    public function set_sheettitle($title) {
+        if (!$title) {
+            return;
+        }
+        $this->sheettitle = $title;
+    }
+
+    /**
+     * Write the start of the format
+     *
+     * @param array $columns
+     */
+    public function write_header($columns) {
+        $this->writer->addRow(array_values((array)$columns));
+    }
+
+    /**
+     * Write a single record
+     *
+     * @param object $record
+     * @param int $rownum
+     */
+    public function write_record($record, $rownum) {
+        $this->writer->addRow(array_values((array)$record));
+    }
+
+    /**
+     * Write the end of the format
+     *
+     * @param array $columns
+     */
+    public function write_footer($columns) {
+        $this->writer->close();
+        $this->writer = null;
+    }
+
+}
index 89efac9..1caa237 100644 (file)
@@ -1758,6 +1758,10 @@ class core_plugin_manager {
                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
             ),
 
+            'dataformat' => array(
+                'html', 'csv', 'json', 'excel', 'ods',
+            ),
+
             'datapreset' => array(
                 'imagegallery'
             ),
@@ -1900,7 +1904,7 @@ class core_plugin_manager {
             'tool' => array(
                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
                 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
-                'langimport', 'log', 'lp', 'lpmigrate', 'messageinbound', 'multilangupgrade', 'monitor',
+                'langimport', 'log', 'lp', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade', 'monitor',
                 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
             ),
diff --git a/lib/classes/plugininfo/dataformat.php b/lib/classes/plugininfo/dataformat.php
new file mode 100644 (file)
index 0000000..6bd48ec
--- /dev/null
@@ -0,0 +1,166 @@
+<?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/>.
+
+/**
+ * Defines classes used for plugin info.
+ *
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @package    core
+ */
+namespace core\plugininfo;
+
+use moodle_url, part_of_admin_tree, admin_settingpage, admin_externalpage, core_plugin_manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for dataformats
+ *
+ * @package    core
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ */
+class dataformat extends base {
+
+    /**
+     * Display name
+     */
+    public function init_display_name() {
+        if (!get_string_manager()->string_exists('dataformat', $this->component)) {
+            $this->displayname = '[dataformat,' . $this->component . ']';
+        } else {
+            $this->displayname = get_string('dataformat', $this->component);
+        }
+    }
+
+    /**
+     * Gathers and returns the information about all plugins of the given type
+     *
+     * @param string $type the name of the plugintype, eg. mod, auth or workshopform
+     * @param string $typerootdir full path to the location of the plugin dir
+     * @param string $typeclass the name of the actually called class
+     * @param core_plugin_manager $pluginman the plugin manager calling this method
+     * @return array of plugintype classes, indexed by the plugin name
+     */
+    public static function get_plugins($type, $typerootdir, $typeclass, $pluginman) {
+        global $CFG;
+        $formats = parent::get_plugins($type, $typerootdir, $typeclass, $pluginman);
+
+        if (!empty($CFG->dataformat_plugins_sortorder)) {
+            $order = explode(',', $CFG->dataformat_plugins_sortorder);
+            $order = array_merge(array_intersect($order, array_keys($formats)),
+                        array_diff(array_keys($formats), $order));
+        } else {
+            $order = array_keys($formats);
+        }
+        $sortedformats = array();
+        foreach ($order as $formatname) {
+            $sortedformats[$formatname] = $formats[$formatname];
+        }
+        return $sortedformats;
+    }
+
+    /**
+     * Finds all enabled plugins, the result may include missing plugins.
+     * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
+     */
+    public static function get_enabled_plugins() {
+        $enabled = array();
+        $plugins = core_plugin_manager::instance()->get_installed_plugins('dataformat');
+
+        if (!$plugins) {
+            return array();
+        }
+
+        $enabled = array();
+        foreach ($plugins as $plugin => $version) {
+            $disabled = get_config('dataformat_' . $plugin, 'disabled');
+            if (empty($disabled)) {
+                $enabled[$plugin] = $plugin;
+            }
+        }
+        return $enabled;
+    }
+
+    /**
+     * Returns the node name used in admin settings menu for this plugin settings (if applicable)
+     *
+     * @return null|string node name or null if plugin does not create settings node (default)
+     */
+    public function get_settings_section_name() {
+        return 'dataformatsetting' . $this->name;
+    }
+
+    /**
+     * Loads plugin settings to the settings tree
+     *
+     * This function usually includes settings.php file in plugins folder.
+     * Alternatively it can create a link to some settings page (instance of admin_externalpage)
+     *
+     * @param \part_of_admin_tree $adminroot
+     * @param string $parentnodename
+     * @param bool $hassiteconfig whether the current user has moodle/site:config capability
+     */
+    public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
+        global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
+        $ADMIN = $adminroot; // May be used in settings.php.
+        $plugininfo = $this; // Also can be used inside settings.php.
+        $dataformat = $this;     // Also can be used inside settings.php.
+
+        if (!$this->is_installed_and_upgraded()) {
+            return;
+        }
+
+        if (!$hassiteconfig) {
+            return;
+        }
+        if (file_exists($this->full_path('settings.php'))) {
+            $fullpath = $this->full_path('settings.php');
+        } else if (file_exists($this->full_path('dataformatsettings.php'))) {
+            $fullpath = $this->full_path('dataformatsettings.php');
+        } else {
+            return;
+        }
+
+        $section = $this->get_settings_section_name();
+        $settings = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
+        include($fullpath); // This may also set $settings to null.
+
+        if ($settings) {
+            $ADMIN->add($parentnodename, $settings);
+        }
+    }
+
+    /**
+     * dataformats can be uninstalled
+     *
+     * @return bool
+     */
+    public function is_uninstall_allowed() {
+        return true;
+    }
+
+    /**
+     * Return URL used for management of plugins of this type.
+     * @return moodle_url
+     */
+    public static function get_manage_url() {
+        return new moodle_url('/admin/settings.php?section=managedataformats');
+    }
+
+}
+
diff --git a/lib/classes/session/redis.php b/lib/classes/session/redis.php
new file mode 100644 (file)
index 0000000..faee370
--- /dev/null
@@ -0,0 +1,351 @@
+<?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/>.
+
+/**
+ * Redis based session handler.
+ *
+ * @package    core
+ * @copyright  2016 Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\session;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Redis based session handler.
+ *
+ * @package    core
+ * @copyright  2016 Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class redis extends handler {
+    /** @var string $savepath save_path string */
+    protected $savepath;
+
+    /** @var array $servers list of servers parsed from save_path */
+    protected $servers;
+
+    /** @var int $acquiretimeout how long to wait for session lock */
+    protected $acquiretimeout = 120;
+
+    /**
+     * Create new instance of handler.
+     */
+    public function __construct() {
+        global $CFG;
+
+        if (!empty($CFG->session_redis_acquire_lock_timeout)) {
+            $this->acquiretimeout = $CFG->session_redis_acquire_lock_timeout;
+        }
+
+        if (empty($CFG->session_redis_save_path)) {
+            $this->savepath = '';
+        } else {
+            $this->savepath = $CFG->session_redis_save_path;
+        }
+
+        if (empty($this->savepath)) {
+            $this->servers = array();
+        } else {
+            $this->servers = $this->connection_string_to_redis_servers($this->savepath);
+        }
+
+    }
+
+    /**
+     * Start the session.
+     * @return bool success
+     */
+    public function start() {
+        $default = ini_get('max_execution_time');
+        set_time_limit($this->acquiretimeout);
+
+        $result = parent::start();
+
+        set_time_limit($default);
+
+        return $result;
+    }
+
+    /**
+     * Init session handler.
+     */
+    public function init() {
+        if (!extension_loaded('Redis')) {
+            throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension is not loaded');
+        }
+
+        // The session handler requires a version of Redis with the SETEX command (at least 2.0).
+        $version = phpversion('Redis');
+        if (!$version or version_compare($version, '2.0') <= 0) {
+            throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension version must be at least 2.0');
+        }
+
+        if (empty($this->savepath)) {
+            throw new exception('sessionhandlerproblem', 'error', '', null,
+                '$CFG->session_redis_save_path must be specified in config.php');
+        }
+
+        ini_set('session.save_handler', 'redis');
+        ini_set('session.save_path', $this->savepath);
+    }
+
+    /**
+     * Check the backend contains data for this session id.
+     *
+     * Note: this is intended to be called from manager::session_exists() only.
+     *
+     * @param string $sid
+     * @return bool true if session found.
+     */
+    public function session_exists($sid) {
+        if (!$this->servers) {
+            return false;
+        }
+
+        foreach ($this->servers as $server) {
+            if ($redis = $this->redis_connect($server)) {
+                $value = $redis->get($server['prefix'] . $sid);
+                $redis->close();
+            }
+
+            if ($value !== false) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Kill all active sessions, the core sessions table is
+     * purged afterwards.
+     */
+    public function kill_all_sessions() {
+        global $DB;
+        if (!$this->servers) {
+            return false;
+        }
+
+        $serverlist = array();
+        foreach ($this->servers as $server) {
+            if ($redis = $this->redis_connect($server)) {
+                $serverlist[] = array($redis, $server['prefix']);
+            }
+        }
+
+        $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
+        foreach ($rs as $record) {
+            foreach ($serverlist as $arr) {
+                list($server, $prefix) = $arr;
+                $server->delete($prefix . $sid);
+            }
+        }
+
+        foreach ($serverlist as $arr) {
+            list($server, $prefix) = $arr;
+            $server->close();
+        }
+    }
+
+    /**
+     * Kill one session, the session record is removed afterwards.
+     * @param string $sid
+     */
+    public function kill_session($sid) {
+        if (!$this->servers) {
+            return false;
+        }
+
+        // Go through the list of all servers because
+        // we do not know where the session handler put the
+        // data.
+
+        foreach ($this->servers as $server) {
+            if ($redis = $this->redis_connect($server)) {
+                $redis->delete($server['prefix'] . $sid);
+                $redis->close();
+            }
+        }
+    }
+
+    /**
+     * Convert a connection string to an array of servers
+     *
+     * Example conversion,
+     * "tcp://host1:123?database=0, unix:///var/run/redis/redis.sock?database=0" to
+     *
+     *  array(
+     *      (
+     *          [scheme]   => 'tcp',
+     *          [host]     => 'host1',
+     *          [port]     => 123,
+     *          [database] => 0,
+     *          [prefix]   => 'PHPREDIS_SESSION:'
+     *      ),
+     *      (
+     *          [scheme]   => 'unix',
+     *          [path]     => '/var/run/redis/redis.sock',
+     *          [database] => 0,
+     *          [prefix]   => 'PHPREDIS_SESSION:'
+     *      )
+     *  )
+     *
+     * @copyright  2016 Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+     * @author     Nicholas Hoobin
+     *
+     * @param string $str save_path value containing redis connection string
+     * @return array
+     */
+    public function connection_string_to_redis_servers($str) {
+        $servers     = array();
+        $connections = array_map('trim', explode(',', $str));
+
+        foreach ($connections as $con) {
+            if (strpos($con, "unix:///") !== false) {
+                $fields = $this->parse_unix_sock($con);
+
+            } else if (strpos($con, "tcp://") !== false) {
+                $fields = $this->parse_url_tcp($con);
+
+            } else {
+                $fields = false;
+                debugging("Invalid Redis schema in connection savepath");
+
+            }
+
+            // Parsing failed.
+            if ($fields === false) {
+                continue;
+            }
+
+            // Setting the default prefix.
+            if (!isset($fields['prefix'])) {
+                $fields['prefix'] = 'PHPREDIS_SESSION:';
+            }
+
+            // Setting the default database.
+            if (!isset($fields['database'])) {
+                $fields['database'] = 0;
+            }
+
+            // Setting the default timeout.
+            if (!isset($fields['timeout'])) {
+                $fields['timeout'] = 86400;
+            }
+
+            $servers[] = $fields;
+        }
+
+        return $servers;
+    }
+
+    /**
+     * Parses the tcp connection string and returns an object.
+     * @param string $con connection string
+     * @return object $con connection data object
+     */
+    private function parse_url_tcp($con) {
+        $con = parse_url($con);
+
+        // Seriously wrong url, parsing failed.
+        if ($con === false) {
+            return false;
+        }
+
+        // Parsing the query string.
+        if (isset($con['query'])) {
+            $query = $con['query'];
+            $parts = explode('&', $query);
+
+            foreach ($parts as $part) {
+                list($key, $value) = explode('=', $part);
+                $con[$key] = $value;
+            }
+        }
+
+        // Setting the default port.
+        if (!isset($con['port'])) {
+            $con['port'] = 6379;
+        }
+
+        return $con;
+    }
+
+    /**
+     * Parses the unix domain socket connection string and returns an object.
+     * @param string $con connection string
+     * @return object $con connection data object
+     */
+    private function parse_unix_sock($con) {
+        // Lets use parse_url to get the bits we need.
+        // To use this, replace the three slashes with two slashes.
+        $con = str_replace(":///", "://", $con);
+        $con = parse_url($con);
+
+        // Seriously wrong url, parsing failed.
+        if ($con === false) {
+            return false;
+        }
+
+        /* Eg. host = var
+               path = run/redis/redis.sock
+               new path = /var/run/redis/redis.sock
+        */
+        $con['path'] = '/' . $con['host'] . $con['path'];
+        unset($con['host']);
+
+        // Parsing the query string.
+        if (isset($con['query'])) {
+            $query = $con['query'];
+            $parts = explode('&', $query);
+
+            foreach ($parts as $part) {
+                list($key, $value) = explode('=', $part);
+                $con[$key] = $value;
+            }
+        }
+
+        return $con;
+    }
+
+    /**
+     * Connects to the Redis server with the details from the connection object.
+     * @param object $con connection details object
+     * @return redis $redis redis connection
+     */
+    private function redis_connect($con) {
+        $redis = new \Redis();
+
+        $func = isset($con['persistent']) ? 'pconnect' : 'connect';
+
+        if ($con['scheme'] === 'tcp') {
+            // Only TCP connections will have a port, default 6379.
+            $result = $redis->$func($con['host'], $con['port'], $con['timeout']);
+        } else if ($con['scheme'] === 'unix') {
+            // Unix domain socket.
+            $result = $redis->$func($con['path']);
+        }
+
+        $result = true ? $redis->select($con['database']) : false;
+
+        return $redis;
+    }
+}
+
index 07c1faf..c06a84c 100644 (file)
@@ -281,10 +281,21 @@ class core_user {
     /**
      * Definition of user profile fields and the expected parameter type for data validation.
      *
+     * array(
+     *     'property_name' => array(       // The user property to be checked. Should match the field on the user table.
+     *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
+     *          'type' => PARAM_TYPE,      // Expected parameter type of the user field.
+     *          'choices' => array(1, 2..) // An array of accepted values of the user field.
+     *          'default' => $CFG->setting // An default value for the field.
+     *     )
+     * )
+     *
+     * The fields choices and default are optional.
+     *
      * @return void
      */
     protected static function fill_properties_cache() {
-
+        global $CFG;
         if (self::$propertiescache !== null) {
             return;
         }
@@ -292,60 +303,70 @@ class core_user {
         // Array of user fields properties and expected parameters.
         // Every new field on the user table should be added here otherwise it won't be validated.
         $fields = array();
-        $fields['id'] = array('type' => PARAM_INT);
-        $fields['auth'] = array('type' => PARAM_NOTAGS);
-        $fields['confirmed'] = array('type' => PARAM_BOOL);
-        $fields['policyagreed'] = array('type' => PARAM_BOOL);
-        $fields['deleted'] = array('type' => PARAM_BOOL);
-        $fields['suspended'] = array('type' => PARAM_BOOL);
-        $fields['mnethostid'] = array('type' => PARAM_BOOL);
-        $fields['username'] = array('type' => PARAM_USERNAME);
-        $fields['password'] = array('type' => PARAM_NOTAGS);
-        $fields['idnumber'] = array('type' => PARAM_NOTAGS);
-        $fields['firstname'] = array('type' => PARAM_NOTAGS);
-        $fields['lastname'] = array('type' => PARAM_NOTAGS);
-        $fields['surname'] = array('type' => PARAM_NOTAGS);
-        $fields['email'] = array('type' => PARAM_RAW_TRIMMED);
-        $fields['emailstop'] = array('type' => PARAM_INT);
-        $fields['icq'] = array('type' => PARAM_NOTAGS);
-        $fields['skype'] = array('type' => PARAM_NOTAGS);
-        $fields['aim'] = array('type' => PARAM_NOTAGS);
-        $fields['yahoo'] = array('type' => PARAM_NOTAGS);
-        $fields['msn'] = array('type' => PARAM_NOTAGS);
-        $fields['phone1'] = array('type' => PARAM_NOTAGS);
-        $fields['phone2'] = array('type' => PARAM_NOTAGS);
-        $fields['institution'] = array('type' => PARAM_TEXT);
-        $fields['department'] = array('type' => PARAM_TEXT);
-        $fields['address'] = array('type' => PARAM_TEXT);
-        $fields['city'] = array('type' => PARAM_TEXT);
-        $fields['country'] = array('type' => PARAM_TEXT);
-        $fields['lang'] = array('type' => PARAM_TEXT);
-        $fields['calendartype'] = array('type' => PARAM_NOTAGS);
-        $fields['theme'] = array('type' => PARAM_NOTAGS);
-        $fields['timezones'] = array('type' => PARAM_TEXT);
-        $fields['firstaccess'] = array('type' => PARAM_INT);
-        $fields['lastaccess'] = array('type' => PARAM_INT);
-        $fields['lastlogin'] = array('type' => PARAM_INT);
-        $fields['currentlogin'] = array('type' => PARAM_INT);
-        $fields['lastip'] = array('type' => PARAM_NOTAGS);
-        $fields['secret'] = array('type' => PARAM_TEXT);
-        $fields['picture'] = array('type' => PARAM_INT);
-        $fields['url'] = array('type' => PARAM_URL);
-        $fields['description'] = array('type' => PARAM_CLEANHTML);
-        $fields['descriptionformat'] = array('type' => PARAM_INT);
-        $fields['mailformat'] = array('type' => PARAM_INT);
-        $fields['maildigest'] = array('type' => PARAM_INT);
-        $fields['maildisplay'] = array('type' => PARAM_INT);
-        $fields['autosubscribe'] = array('type' => PARAM_INT);
-        $fields['trackforums'] = array('type' => PARAM_INT);
-        $fields['timecreated'] = array('type' => PARAM_INT);
-        $fields['timemodified'] = array('type' => PARAM_INT);
-        $fields['trustbitmask'] = array('type' => PARAM_INT);
-        $fields['imagealt'] = array('type' => PARAM_TEXT);
-        $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS);
-        $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS);
-        $fields['middlename'] = array('type' => PARAM_NOTAGS);
-        $fields['alternatename'] = array('type' => PARAM_NOTAGS);
+        $fields['id'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['auth'] = array('type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED);
+        $fields['confirmed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
+        $fields['policyagreed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
+        $fields['deleted'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
+        $fields['suspended'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
+        $fields['mnethostid'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['username'] = array('type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED);
+        $fields['password'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
+        $fields['idnumber'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
+        $fields['firstname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['lastname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['surname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['email'] = array('type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED);
+        $fields['emailstop'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['icq'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['skype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
+        $fields['aim'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['yahoo'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['msn'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['phone1'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['phone2'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['institution'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
+        $fields['department'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
+        $fields['address'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
+        $fields['city'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity);
+        $fields['country'] = array('type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->country,
+                'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
+        $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->lang,
+                'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
+        $fields['calendartype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
+                'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
+        $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
+                'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
+        $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->timezone,
+                'choices' => core_date::get_list_of_timezones(null, true));
+        $fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['currentlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['lastip'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
+        $fields['secret'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
+        $fields['picture'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['url'] = array('type' => PARAM_URL, 'null' => NULL_NOT_ALLOWED);
+        $fields['description'] = array('type' => PARAM_RAW, 'null' => NULL_ALLOWED);
+        $fields['descriptionformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['mailformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
+                'default' => $CFG->defaultpreference_mailformat);
+        $fields['maildigest'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
+                'default' => $CFG->defaultpreference_maildigest);
+        $fields['maildisplay'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
+                'default' => $CFG->defaultpreference_maildisplay);
+        $fields['autosubscribe'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
+                'default' => $CFG->defaultpreference_autosubscribe);
+        $fields['trackforums'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
+                'default' => $CFG->defaultpreference_trackforums);
+        $fields['timecreated'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['timemodified'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['trustbitmask'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
+        $fields['imagealt'] = array('type' => PARAM_TEXT, 'null' => NULL_ALLOWED);
+        $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
+        $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
+        $fields['middlename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
+        $fields['alternatename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 
         self::$propertiescache = $fields;
     }
@@ -368,6 +389,37 @@ class core_user {
         return self::$propertiescache[$property];
     }
 
+    /**
+     * Validate user data.
+     *
+     * This method just validates each user field and return an array of errors. It doesn't clean the data,
+     * the methods clean() and clean_field() should be used for this purpose.
+     *
+     * @param stdClass|array $data user data object or array to be validated.
+     * @return array|true $errors array of errors found on the user object, true if the validation passed.
+     */
+    public static function validate($data) {
+        // Get all user profile fields definition.
+        self::fill_properties_cache();
+
+        foreach ($data as $property => $value) {
+            try {
+                if (isset(self::$propertiescache[$property])) {
+                    validate_param($value, self::$propertiescache[$property]['type'], self::$propertiescache[$property]['null']);
+                }
+                // Check that the value is part of a list of allowed values.
+                if (!empty(self::$propertiescache[$property]['choices']) &&
+                        !isset(self::$propertiescache[$property]['choices'][$value])) {
+                    throw new invalid_parameter_exception($value);
+                }
+            } catch (invalid_parameter_exception $e) {
+                $errors[$property] = $e->getMessage();
+            }
+        }
+
+        return empty($errors) ? true : $errors;
+    }
+
     /**
      * Clean the properties cache.
      *
@@ -377,4 +429,141 @@ class core_user {
     public static function reset_caches() {
         self::$propertiescache = null;
     }
+
+    /**
+     * Clean the user data.
+     *
+     * @param stdClass|array $user the user data to be validated against properties definition.
+     * @return stdClass $user the cleaned user data.
+     */
+    public static function clean_data($user) {
+        if (empty($user)) {
+            return $user;
+        }
+
+        foreach ($user as $field => $value) {
+            // Get the property parameter type and do the cleaning.
+            try {
+                $user->$field = core_user::clean_field($value, $field);
+            } catch (coding_exception $e) {
+                debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
+            }
+        }
+
+        return $user;
+    }
+
+    /**
+     * Clean a specific user field.
+     *
+     * @param string $data the user field data to be cleaned.
+     * @param string $field the user field name on the property definition cache.
+     * @return string the cleaned user data.
+     */
+    public static function clean_field($data, $field) {
+        if (empty($data) || empty($field)) {
+            return $data;
+        }
+
+        try {
+            $type = core_user::get_property_type($field);
+
+            if (isset(self::$propertiescache[$field]['choices'])) {
+                if (!array_key_exists($data, self::$propertiescache[$field]['choices'])) {
+                    if (isset(self::$propertiescache[$field]['default'])) {
+                        $data = self::$propertiescache[$field]['default'];
+                    } else {
+                        $data = '';
+                    }
+                } else {
+                    return $data;
+                }
+            } else {
+                $data = clean_param($data, $type);
+            }
+        } catch (coding_exception $e) {
+            debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Get the parameter type of the property.
+     *
+     * @param string $property property name to be retrieved.
+     * @throws coding_exception if the requested property name is invalid.
+     * @return int the property parameter type.
+     */
+    public static function get_property_type($property) {
+
+        self::fill_properties_cache();
+
+        if (!array_key_exists($property, self::$propertiescache)) {
+            throw new coding_exception('Invalid property requested: ' . $property);
+        }
+
+        return self::$propertiescache[$property]['type'];
+    }
+
+    /**
+     * Discover if the property is NULL_ALLOWED or NULL_NOT_ALLOWED.
+     *
+     * @param string $property property name to be retrieved.
+     * @throws coding_exception if the requested property name is invalid.
+     * @return bool true if the property is NULL_ALLOWED, false otherwise.
+     */
+    public static function get_property_null($property) {
+
+        self::fill_properties_cache();
+
+        if (!array_key_exists($property, self::$propertiescache)) {
+            throw new coding_exception('Invalid property requested: ' . $property);
+        }
+
+        return self::$propertiescache[$property]['null'];
+    }
+
+    /**
+     * Get the choices of the property.
+     *
+     * This is a helper method to validate a value against a list of acceptable choices.
+     * For instance: country, timezone, language, themes and etc.
+     *
+     * @param string $property property name to be retrieved.
+     * @throws coding_exception if the requested property name is invalid or if it does not has a list of choices.
+     * @return array the property parameter type.
+     */
+    public static function get_property_choices($property) {
+
+        self::fill_properties_cache();
+
+        if (!array_key_exists($property, self::$propertiescache) && !array_key_exists('choices',
+                self::$propertiescache[$property])) {
+
+            throw new coding_exception('Invalid property requested, or the property does not has a list of choices.');
+        }
+
+        return self::$propertiescache[$property]['choices'];
+    }
+
+    /**
+     * Get the property default.
+     *
+     * This method gets the default value of a field (if exists).
+     *
+     * @param string $property property name to be retrieved.
+     * @throws coding_exception if the requested property name is invalid or if it does not has a default value.
+     * @return string the property default value.
+     */
+    public static function get_property_default($property) {
+
+        self::fill_properties_cache();
+
+        if (!array_key_exists($property, self::$propertiescache) || !isset(self::$propertiescache[$property]['default'])) {
+            throw new coding_exception('Invalid property requested, or the property does not has a default value.');
+        }
+
+        return self::$propertiescache[$property]['default'];
+    }
 }
diff --git a/lib/dataformatlib.php b/lib/dataformatlib.php
new file mode 100644 (file)
index 0000000..36b5173
--- /dev/null
@@ -0,0 +1,72 @@
+<?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/>.
+
+/**
+ * dataformatlib.php - Contains core dataformat related functions.
+ *
+ * @package    core
+ * @subpackage dataformat
+ * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Sends a formated data file to the browser
+ *
+ * @package    core
+ * @subpackage dataformat
+ *
+ * @param string $filename The base filename without an extension
+ * @param string $dataformat A dataformat name
+ * @param array $columns An ordered map of column keys and labels
+ * @param Iterator $iterator An iterator over the records, usually a RecordSet
+ * @param function $callback An option function applied to each record before writing
+ * @param mixed $extra An optional value which is passed into the callback function
+ */
+function download_as_dataformat($filename, $dataformat, $columns, $iterator, $callback = null) {
+
+    if (ob_get_length()) {
+        throw new coding_exception("Output can not be buffered before calling download_as_dataformat");
+    }
+
+    $classname = 'dataformat_' . $dataformat . '\writer';
+    if (!class_exists($classname)) {
+        throw new coding_exception("Unable to locate dataformat/$type/classes/writer.php");
+    }
+    $format = new $classname;
+
+    // The data format export could take a while to generate...
+    set_time_limit(0);
+
+    // Close the session so that the users other tabs in the same session are not blocked.
+    \core\session\manager::write_close();
+
+    $format->set_filename($filename);
+    $format->send_http_headers();
+    $format->write_header($columns);
+    $c = 0;
+    foreach ($iterator as $row) {
+        if ($callback) {
+            $row = $callback($row);
+        }
+        if ($row === null) {
+            continue;
+        }
+        $format->write_record($row, $c++);
+    }
+    $format->write_footer($columns);
+}
+
index 072b3db..e6b6cef 100644 (file)
@@ -1999,5 +1999,11 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2016041500.66);
     }
 
+    if ($oldversion < 2016042100.00) {
+        // Update all countries to upper case.
+        $DB->execute("UPDATE {user} SET country = UPPER(country)");
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016042100.00);
+    }
     return true;
 }
index b9b3d7e..4de979b 100644 (file)
@@ -92,7 +92,7 @@ class core_external extends external_api {
                       new external_single_structure(array(
                           'name' => new external_value(PARAM_ALPHANUMEXT, 'param name
                             - if the string expect only one $a parameter then don\'t send this field, just send the value.', VALUE_OPTIONAL),
-                          'value' => new external_value(PARAM_TEXT,'param value'))),
+                          'value' => new external_value(PARAM_RAW,'param value'))),
                           'the definition of a string param (i.e. {$a->name})', VALUE_DEFAULT, array()
                    )
             )
@@ -124,7 +124,7 @@ class core_external extends external_api {
      * @since Moodle 2.4
      */
     public static function get_string_returns() {
-        return new external_value(PARAM_TEXT, 'translated string');
+        return new external_value(PARAM_RAW, 'translated string');
     }
 
     /**
@@ -144,7 +144,7 @@ class core_external extends external_api {
                             new external_single_structure(array(
                                 'name' => new external_value(PARAM_ALPHANUMEXT, 'param name
                                     - if the string expect only one $a parameter then don\'t send this field, just send the value.', VALUE_OPTIONAL),
-                                'value' => new external_value(PARAM_TEXT, 'param value'))),
+                                'value' => new external_value(PARAM_RAW, 'param value'))),
                                 'the definition of a string param (i.e. {$a->name})', VALUE_DEFAULT, array()
                         ))
                     )
@@ -198,7 +198,7 @@ class core_external extends external_api {
                 'stringid' => new external_value(PARAM_STRINGID, 'string id'),
                 'component' => new external_value(PARAM_COMPONENT, 'string component'),
                 'lang' => new external_value(PARAM_LANG, 'lang'),
-                'string' => new external_value(PARAM_TEXT, 'translated string'))
+                'string' => new external_value(PARAM_RAW, 'translated string'))
             ));
     }
 
index 4eba534..4ff82b0 100644 (file)
@@ -78,6 +78,26 @@ class core_external_testcase extends externallib_advanced_testcase {
                       array('name' => 'id', 'value' => $service->id)));
     }
 
+    /**
+     * Test get_string with HTML.
+     */
+    public function test_get_string_containing_html() {
+        $result = core_external::get_string('registrationinfo');
+        $actual = external_api::clean_returnvalue(core_external::get_string_returns(), $result);
+        $expected = get_string('registrationinfo', 'moodle');
+        $this->assertSame($expected, $actual);
+    }
+
+    /**
+     * Test get_string with arguments containing HTML.
+     */
+    public function test_get_string_with_args_containing_html() {
+        $result = core_external::get_string('added', 'moodle', null, [['value' => '<strong>Test</strong>']]);
+        $actual = external_api::clean_returnvalue(core_external::get_string_returns(), $result);
+        $expected = get_string('added', 'moodle', '<strong>Test</strong>');
+        $this->assertSame($expected, $actual);
+    }
+
     /**
      * Test get_strings
      */
@@ -114,6 +134,29 @@ class core_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test get_strings with HTML.
+     */
+    public function test_get_strings_containing_html() {
+        $result = core_external::get_strings([['stringid' => 'registrationinfo'], ['stringid' => 'loginaspasswordexplain']]);
+        $actual = external_api::clean_returnvalue(core_external::get_strings_returns(), $result);
+        $this->assertSame(get_string('registrationinfo', 'moodle'), $actual[0]['string']);
+        $this->assertSame(get_string('loginaspasswordexplain', 'moodle'), $actual[1]['string']);
+    }
+
+    /**
+     * Test get_strings with arguments containing HTML.
+     */
+    public function test_get_strings_with_args_containing_html() {
+        $result = core_external::get_strings([
+            ['stringid' => 'added', 'stringparams' => [['value' => '<strong>Test</strong>']]],
+            ['stringid' => 'loggedinas', 'stringparams' => [['value' => '<strong>Test</strong>']]]]
+        );
+        $actual = external_api::clean_returnvalue(core_external::get_strings_returns(), $result);
+        $this->assertSame(get_string('added', 'moodle', '<strong>Test</strong>'), $actual[0]['string']);
+        $this->assertSame(get_string('loggedinas', 'moodle', '<strong>Test</strong>'), $actual[1]['string']);
+    }
+
     /**
      * Test get_component_strings
      */
index 2723f70..7ad1284 100644 (file)
@@ -813,34 +813,108 @@ class grade_category extends grade_object {
     private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
         global $DB;
 
-        // Reset aggregation to unknown and 0 for all grade items for this user and category.
+        // We want to know all current user grades so we can decide whether they need to be updated or they already contain the
+        // expected value.
+        $sql = "SELECT gi.id, gg.aggregationstatus, gg.aggregationweight FROM {grade_grades} gg
+                  JOIN {grade_items} gi ON (gg.itemid = gi.id)
+                 WHERE gg.userid = :userid";
         $params = array('categoryid' => $this->id, 'userid' => $userid);
 
-        switch ($DB->get_dbfamily()) {
-            case 'mysql':
-                // Optimize the query for MySQL by using a join rather than a sub-query.
-                $sql = "UPDATE {grade_grades} g
-                          JOIN {grade_items} gi ON (g.itemid = gi.id)
-                           SET g.aggregationstatus = 'unknown',
-                               g.aggregationweight = 0
-                         WHERE g.userid = :userid
-                           AND gi.categoryid = :categoryid";
-                break;
-            default:
-                $itemssql = "SELECT id
-                               FROM {grade_items}
-                              WHERE categoryid = :categoryid";
+        // These are all grade_item ids which grade_grades will NOT end up being 'unknown' (because they are not unknown or
+        // because we will update them to something different that 'unknown').
+        $giids = array_keys($usedweights + $novalue + $dropped + $extracredit);
 
-                $sql = "UPDATE {grade_grades}
-                           SET aggregationstatus = 'unknown',
-                               aggregationweight = 0
-                         WHERE userid = :userid
-                           AND itemid IN ($itemssql)";
+        if ($giids) {
+            // We include grade items that might not be in categoryid.
+            list($itemsql, $itemlist) = $DB->get_in_or_equal($giids, SQL_PARAMS_NAMED, 'gg');
+            $sql .= ' AND (gi.categoryid = :categoryid OR gi.id ' . $itemsql . ')';
+            $params = $params + $itemlist;
+        } else {
+            $sql .= ' AND gi.categoryid = :categoryid';
         }
+        $currentgrades = $DB->get_recordset_sql($sql, $params);
 
-        $DB->execute($sql, $params);
+        // We will store here the grade_item ids that need to be updated on db.
+        $toupdate = array();
+
+        if ($currentgrades->valid()) {
+
+            // Iterate through the user grades to see if we really need to update any of them.
+            foreach ($currentgrades as $currentgrade) {
+
+                // Unset $usedweights that we do not need to update.
+                if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) && $currentgrade->aggregationstatus === 'used') {
+                    // We discard the ones that already have the contribution specified in $usedweights and are marked as 'used'.
+                    if (grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) {
+                        unset($usedweights[$currentgrade->id]);
+                    }
+                    // Used weights can be present in multiple set_usedinaggregation arguments.
+                    if (!isset($novalue[$currentgrade->id]) && !isset($dropped[$currentgrade->id]) &&
+                            !isset($extracredit[$currentgrade->id])) {
+                        continue;
+                    }
+                }
+
+                // No value grades.
+                if (!empty($novalue) && isset($novalue[$currentgrade->id])) {
+                    if ($currentgrade->aggregationstatus !== 'novalue' ||
+                            grade_floats_different($currentgrade->aggregationweight, 0)) {
+                        $toupdate['novalue'][] = $currentgrade->id;
+                    }
+                    continue;
+                }
+
+                // Dropped grades.
+                if (!empty($dropped) && isset($dropped[$currentgrade->id])) {
+                    if ($currentgrade->aggregationstatus !== 'dropped' ||
+                            grade_floats_different($currentgrade->aggregationweight, 0)) {
+                        $toupdate['dropped'][] = $currentgrade->id;
+                    }
+                    continue;
+                }
+
+                // Extra credit grades.
+                if (!empty($extracredit) && isset($extracredit[$currentgrade->id])) {
+
+                    // If this grade item is already marked as 'extra' and it already has the provided $usedweights value would be
+                    // silly to update to 'used' to later update to 'extra'.
+                    if (!empty($usedweights) && isset($usedweights[$currentgrade->id]) &&
+                            grade_floats_equal($currentgrade->aggregationweight, $usedweights[$currentgrade->id])) {
+                        unset($usedweights[$currentgrade->id]);
+                    }
+
+                    // Update the item to extra if it is not already marked as extra in the database or if the item's
+                    // aggregationweight will be updated when going through $usedweights items.
+                    if ($currentgrade->aggregationstatus !== 'extra' ||
+                            (!empty($usedweights) && isset($usedweights[$currentgrade->id]))) {
+                        $toupdate['extracredit'][] = $currentgrade->id;
+                    }
+                    continue;
+                }
+
+                // If is not in any of the above groups it should be set to 'unknown', checking that the item is not already
+                // unknown, if it is we don't need to update it.
+                if ($currentgrade->aggregationstatus !== 'unknown' || grade_floats_different($currentgrade->aggregationweight, 0)) {
+                    $toupdate['unknown'][] = $currentgrade->id;
+                }
+            }
+            $currentgrades->close();
+        }
+
+        // Update items to 'unknown' status.
+        if (!empty($toupdate['unknown'])) {
+            list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['unknown'], SQL_PARAMS_NAMED, 'g');
+
+            $itemlist['userid'] = $userid;
+
+            $sql = "UPDATE {grade_grades}
+                       SET aggregationstatus = 'unknown',
+                           aggregationweight = 0
+                     WHERE itemid $itemsql AND userid = :userid";
+            $DB->execute($sql, $itemlist);
+        }
 
-        // Included with weights.
+        // Update items to 'used' status and setting the proper weight.
         if (!empty($usedweights)) {
             // The usedweights items are updated individually to record the weights.
             foreach ($usedweights as $gradeitemid => $contribution) {
@@ -854,9 +928,9 @@ class grade_category extends grade_object {
             }
         }
 
-        // No value.
-        if (!empty($novalue)) {
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($novalue), SQL_PARAMS_NAMED, 'g');
+        // Update items to 'novalue' status.
+        if (!empty($toupdate['novalue'])) {
+            list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['novalue'], SQL_PARAMS_NAMED, 'g');
 
             $itemlist['userid'] = $userid;
 
@@ -868,9 +942,9 @@ class grade_category extends grade_object {
             $DB->execute($sql, $itemlist);
         }
 
-        // Dropped.
-        if (!empty($dropped)) {
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($dropped), SQL_PARAMS_NAMED, 'g');
+        // Update items to 'dropped' status.
+        if (!empty($toupdate['dropped'])) {
+            list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['dropped'], SQL_PARAMS_NAMED, 'g');
 
             $itemlist['userid'] = $userid;
 
@@ -882,9 +956,9 @@ class grade_category extends grade_object {
             $DB->execute($sql, $itemlist);
         }
 
-        // Extra credit.
-        if (!empty($extracredit)) {
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($extracredit), SQL_PARAMS_NAMED, 'g');
+        // Update items to 'extracredit' status.
+        if (!empty($toupdate['extracredit'])) {
+            list($itemsql, $itemlist) = $DB->get_in_or_equal($toupdate['extracredit'], SQL_PARAMS_NAMED, 'g');
 
             $itemlist['userid'] = $userid;
 
index 190c344..cdc1402 100644 (file)
@@ -290,7 +290,7 @@ class grade_scale extends grade_object {
         }
 
         // Ask the competency subsystem.
-        if (\core_competency\api::is_scale_used_anywhere($scaleid)) {
+        if (\core_competency\api::is_scale_used_anywhere($this->id)) {
             return true;
         }
 
index 5294b94..86201f1 100644 (file)
@@ -282,6 +282,10 @@ class core_grade_category_testcase extends grade_base_testcase {
         $DB->set_field('grade_grades', 'finalgrade', null, array('itemid'=>$childcat1itemid));
         $parentcat->generate_grades();
 
+        $this->assertFalse($DB->record_exists_select(
+                                     'grade_grades',
+                                     "aggregationstatus='dropped' and itemid in (?,?)",
+                                     array($childcat1itemid, $childcat2itemid)));
         $this->assertTrue($DB->record_exists_select(
                                      'grade_grades',
                                      "aggregationstatus='novalue' and itemid = ?",
index b65c898..afb15b8 100644 (file)
@@ -3919,7 +3919,7 @@ class settings_navigation extends navigation_node {
         // Reset this course
         if (has_capability('moodle/course:reset', $coursecontext)) {
             $url = new moodle_url('/course/reset.php', array('id'=>$course->id));
-            $coursenode->add(get_string('reset'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/return', ''));
+            $coursenode->add(get_string('reset'), $url, self::TYPE_SETTING, null, 'reset', new pix_icon('i/return', ''));
         }
 
         // Questions
@@ -3969,7 +3969,7 @@ class settings_navigation extends navigation_node {
             }
         }
         if (is_array($roles) && count($roles)>0) {
-            $switchroles = $this->add(get_string('switchroleto'));
+            $switchroles = $this->add(get_string('switchroleto'), null, self::TYPE_CONTAINER, null, 'switchroleto');
             if ((count($roles)==1 && array_key_exists(0, $roles))|| $assumedrole!==false) {
                 $switchroles->force_open();
             }
index c8e75b5..9243880 100644 (file)
@@ -1062,7 +1062,13 @@ class core_renderer extends renderer_base {
         }
         $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
 
-        $this->page->requires->js_call_amd('core/notification', 'init', array($PAGE->context->id, \core\notification::fetch_as_array($this)));
+        // Only show notifications when we have a $PAGE context id.
+        if (!empty($PAGE->context->id)) {
+            $this->page->requires->js_call_amd('core/notification', 'init', array(
+                $PAGE->context->id,
+                \core\notification::fetch_as_array($this)
+            ));
+        }
         $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
 
         $this->page->set_state(moodle_page::STATE_DONE);
@@ -1876,6 +1882,48 @@ class core_renderer extends renderer_base {
         return $this->render($select);
     }
 
+    /**
+     * Returns a dataformat selection and download form
+     *
+     * @param string $label A text label
+     * @param moodle_url|string $base The download page url
+     * @param string $name The query param which will hold the type of the download
+     * @param array $params Extra params sent to the download page
+     * @return string HTML fragment
+     */
+    public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array()) {
+
+        $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
+        $options = array();
+        foreach ($formats as $format) {
+            if ($format->is_enabled()) {
+                $options[] = array(
+                    'value' => $format->name,
+                    'label' => get_string('dataformat', $format->component),
+                );
+            }
+        }
+        $hiddenparams = array();
+        foreach ($params as $key => $value) {
+            $hiddenparams[] = array(
+                'name' => $key,
+                'value' => $value,
+            );
+        }
+        $data = array(
+            'label' => $label,
+            'base' => $base,
+            'name' => $name,
+            'params' => $hiddenparams,
+            'options' => $options,
+            'sesskey' => sesskey(),
+            'submit' => get_string('download'),
+        );
+
+        return $this->render_from_template('core/dataformat_selector', $data);
+    }
+
+
     /**
      * Internal implementation of single_select rendering
      *
index a4fbf68..0179887 100644 (file)
@@ -1388,24 +1388,24 @@ function question_extend_settings_navigation(navigation_node $navigationnode, $c
     }
 
     $questionnode = $navigationnode->add(get_string('questionbank', 'question'),
-            new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER);
+            new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER, null, 'questionbank');
 
     $contexts = new question_edit_contexts($context);
     if ($contexts->have_one_edit_tab_cap('questions')) {
         $questionnode->add(get_string('questions', 'question'), new moodle_url(
-                '/question/edit.php', $params), navigation_node::TYPE_SETTING);
+                '/question/edit.php', $params), navigation_node::TYPE_SETTING, null, 'questions');
     }
     if ($contexts->have_one_edit_tab_cap('categories')) {
         $questionnode->add(get_string('categories', 'question'), new moodle_url(
-                '/question/category.php', $params), navigation_node::TYPE_SETTING);
+                '/question/category.php', $params), navigation_node::TYPE_SETTING, null, 'categories');
     }
     if ($contexts->have_one_edit_tab_cap('import')) {
         $questionnode->add(get_string('import', 'question'), new moodle_url(
-                '/question/import.php', $params), navigation_node::TYPE_SETTING);
+                '/question/import.php', $params), navigation_node::TYPE_SETTING, null, 'import');
     }
     if ($contexts->have_one_edit_tab_cap('export')) {
         $questionnode->add(get_string('export', 'question'), new moodle_url(
-                '/question/export.php', $params), navigation_node::TYPE_SETTING);
+                '/question/export.php', $params), navigation_node::TYPE_SETTING, null, 'export');
     }
 
     return $questionnode;
diff --git a/lib/spout/LICENSE b/lib/spout/LICENSE
new file mode 100644 (file)
index 0000000..167ec4d
--- /dev/null
@@ -0,0 +1,166 @@
+                             Apache License
+                       Version 2.0, January 2004
+                    http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+  "License" shall mean the terms and conditions for use, reproduction,
+  and distribution as defined by Sections 1 through 9 of this document.
+  "Licensor" shall mean the copyright owner or entity authorized by
+  the copyright owner that is granting the License.
+  "Legal Entity" shall mean the union of the acting entity and all
+  other entities that control, are controlled by, or are under common
+  control with that entity. For the purposes of this definition,
+  "control" means (i) the power, direct or indirect, to cause the
+  direction or management of such entity, whether by contract or
+  otherwise, or (ii) ownership of fifty percent (50%) or more of the
+  outstanding shares, or (iii) beneficial ownership of such entity.
+  "You" (or "Your") shall mean an individual or Legal Entity
+  exercising permissions granted by this License.
+  "Source" form shall mean the preferred form for making modifications,
+  including but not limited to software source code, documentation
+  source, and configuration files.
+  "Object" form shall mean any form resulting from mechanical
+  transformation or translation of a Source form, including but
+  not limited to compiled object code, generated documentation,
+  and conversions to other media types.
+  "Work" shall mean the work of authorship, whether in Source or
+  Object form, made available under the License, as indicated by a
+  copyright notice that is included in or attached to the work
+  (an example is provided in the Appendix below).
+  "Derivative Works" shall mean any work, whether in Source or Object
+  form, that is based on (or derived from) the Work and for which the
+  editorial revisions, annotations, elaborations, or other modifications
+  represent, as a whole, an original work of authorship. For the purposes
+  of this License, Derivative Works shall not include works that remain
+  separable from, or merely link (or bind by name) to the interfaces of,
+  the Work and Derivative Works thereof.
+  "Contribution" shall mean any work of authorship, including
+  the original version of the Work and any modifications or additions
+  to that Work or Derivative Works thereof, that is intentionally
+  submitted to Licensor for inclusion in the Work by the copyright owner
+  or by an individual or Legal Entity authorized to submit on behalf of
+  the copyright owner. For the purposes of this definition, "submitted"
+  means any form of electronic, verbal, or written communication sent
+  to the Licensor or its representatives, including but not limited to
+  communication on electronic mailing lists, source code control systems,
+  and issue tracking systems that are managed by, or on behalf of, the
+  Licensor for the purpose of discussing and improving the Work, but
+  excluding communication that is conspicuously marked or otherwise
+  designated in writing by the copyright owner as "Not a Contribution."
+  "Contributor" shall mean Licensor and any individual or Legal Entity
+  on behalf of whom a Contribution has been received by Licensor and
+  subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  copyright license to reproduce, prepare Derivative Works of,
+  publicly display, publicly perform, sublicense, and distribute the
+  Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  (except as stated in this section) patent license to make, have made,
+  use, offer to sell, sell, import, and otherwise transfer the Work,
+  where such license applies only to those patent claims licensable
+  by such Contributor that are necessarily infringed by their
+  Contribution(s) alone or by combination of their Contribution(s)
+  with the Work to which such Contribution(s) was submitted. If You
+  institute patent litigation against any entity (including a
+  cross-claim or counterclaim in a lawsuit) alleging that the Work
+  or a Contribution incorporated within the Work constitutes direct
+  or contributory patent infringement, then any patent licenses
+  granted to You under this License for that Work shall terminate
+  as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+  Work or Derivative Works thereof in any medium, with or without
+  modifications, and in Source or Object form, provided that You
+  meet the following conditions:
+
+  (a) You must give any other recipients of the Work or
+      Derivative Works a copy of this License; and
+
+  (b) You must cause any modified files to carry prominent notices
+      stating that You changed the files; and
+
+  (c) You must retain, in the Source form of any Derivative Works
+      that You distribute, all copyright, patent, trademark, and
+      attribution notices from the Source form of the Work,
+      excluding those notices that do not pertain to any part of
+      the Derivative Works; and
+
+  (d) If the Work includes a "NOTICE" text file as part of its
+      distribution, then any Derivative Works that You distribute must
+      include a readable copy of the attribution notices contained
+      within such NOTICE file, excluding those notices that do not
+      pertain to any part of the Derivative Works, in at least one
+      of the following places: within a NOTICE text file distributed
+      as part of the Derivative Works; within the Source form or
+      documentation, if provided along with the Derivative Works; or,
+      within a display generated by the Derivative Works, if and
+      wherever such third-party notices normally appear. The contents
+      of the NOTICE file are for informational purposes only and
+      do not modify the License. You may add Your own attribution
+      notices within Derivative Works that You distribute, alongside
+      or as an addendum to the NOTICE text from the Work, provided
+      that such additional attribution notices cannot be construed
+      as modifying the License.
+
+  You may add Your own copyright statement to Your modifications and
+  may provide additional or different license terms and conditions
+  for use, reproduction, or distribution of Your modifications, or
+  for any such Derivative Works as a whole, provided Your use,
+  reproduction, and distribution of the Work otherwise complies with
+  the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+  any Contribution intentionally submitted for inclusion in the Work
+  by You to the Licensor shall be under the terms and conditions of
+  this License, without any additional terms or conditions.
+  Notwithstanding the above, nothing herein shall supersede or modify
+  the terms of any separate license agreement you may have executed
+  with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+  names, trademarks, service marks, or product names of the Licensor,
+  except as required for reasonable and customary use in describing the
+  origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+  agreed to in writing, Licensor provides the Work (and each
+  Contributor provides its Contributions) on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  implied, including, without limitation, any warranties or conditions
+  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+  PARTICULAR PURPOSE. You are solely responsible for determining the
+  appropriateness of using or redistributing the Work and assume any
+  risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+  whether in tort (including negligence), contract, or otherwise,
+  unless required by applicable law (such as deliberate and grossly
+  negligent acts) or agreed to in writing, shall any Contributor be
+  liable to You for damages, including any direct, indirect, special,
+  incidental, or consequential damages of any character arising as a
+  result of this License or out of the use or inability to use the
+  Work (including but not limited to damages for loss of goodwill,
+  work stoppage, computer failure or malfunction, or any and all
+  other commercial damages or losses), even if such Contributor
+  has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+  the Work or Derivative Works thereof, You may choose to offer,
+  and charge a fee for, acceptance of support, warranty, indemnity,
+  or other liability obligations and/or rights consistent with this
+  License. However, in accepting such obligations, You may act only
+  on Your own behalf and on Your sole responsibility, not on behalf
+  of any other Contributor, and only if You agree to indemnify,
+  defend, and hold each Contributor harmless for any liability
+  incurred by, or claims asserted against, such Contributor by reason
+  of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
diff --git a/lib/spout/README.md b/lib/spout/README.md
new file mode 100644 (file)
index 0000000..6bb73aa
--- /dev/null
@@ -0,0 +1,338 @@
+# Spout
+
+[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout)
+[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges)
+[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout)
+[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master)
+[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
+[![License](https://poser.pugx.org/box/spout/license)](https://packagist.org/packages/box/spout)
+
+Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
+Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
+
+Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+
+## Installation
+
+### Composer (recommended)
+
+Spout can be installed directly from [Composer](https://getcomposer.org/).
+
+Run the following command:
+```
+$ composer require box/spout
+```
+
+### Manual installation
+
+If you can't use Composer, no worries! You can still install Spout manually.
+
+> Before starting, make sure your system meets the [requirements](#requirements).
+
+1. Download the source code from the [Releases page](https://github.com/box/spout/releases)
+2. Extract the downloaded content into your project.
+3. Add this code to the top controller (index.php) or wherever it may be more appropriate:
+```php
+require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path!
+```
+
+
+## Requirements
+
+* PHP version 5.4.0 or higher
+* PHP extension `php_zip` enabled
+* PHP extension `php_xmlreader` enabled
+* PHP extension `php_simplexml` enabled
+
+
+## Basic usage
+
+### Reader
+
+Regardless of the file type, the interface to read a file is always the same:
+
+```php
+use Box\Spout\Reader\ReaderFactory;
+use Box\Spout\Common\Type;
+
+$reader = ReaderFactory::create(Type::XLSX); // for XLSX files
+//$reader = ReaderFactory::create(Type::CSV); // for CSV files
+//$reader = ReaderFactory::create(Type::ODS); // for ODS files
+
+$reader->open($filePath);
+
+foreach ($reader->getSheetIterator() as $sheet) {
+    foreach ($sheet->getRowIterator() as $row) {
+        // do stuff with the row
+    }
+}
+
+$reader->close();
+```
+
+If there are multiple sheets in the file, the reader will read all of them sequentially.
+
+### Writer
+
+As with the reader, there is one common interface to write data to a file:
+
+```php
+use Box\Spout\Writer\WriterFactory;
+use Box\Spout\Common\Type;
+
+$writer = WriterFactory::create(Type::XLSX); // for XLSX files
+//$writer = WriterFactory::create(Type::CSV); // for CSV files
+//$writer = WriterFactory::create(Type::ODS); // for ODS files
+
+$writer->openToFile($filePath); // write data to a file or to a PHP stream
+//$writer->openToBrowser($fileName); // stream data directly to the browser
+
+$writer->addRow($singleRow); // add a row at a time
+$writer->addRows($multipleRows); // add multiple rows at a time
+
+$writer->close();
+```
+
+For XLSX and ODS files, the number of rows per sheet is limited to 1,048,576. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it.
+
+
+## Advanced usage
+
+If you are looking for  how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used.
+
+### Configuring the CSV reader and writer
+
+It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure:
+```php
+use Box\Spout\Reader\ReaderFactory;
+use Box\Spout\Common\Type;
+
+$reader = ReaderFactory::create(Type::CSV);
+$reader->setFieldDelimiter('|');
+$reader->setFieldEnclosure('@');
+$reader->setEndOfLineCharacter("\r");
+```
+
+Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
+```php
+$reader->setEncoding('UTF-16LE');
+```
+
+The writer always generate CSV files encoded in UTF-8, with a BOM.
+
+
+### Configuring the XLSX and ODS writers
+
+#### Row styling
+
+It is possible to apply some formatting options to a row. Spout supports fonts as well as alignment styles.
+
+```php
+use Box\Spout\Common\Type;
+use Box\Spout\Writer\WriterFactory;
+use Box\Spout\Writer\Style\StyleBuilder;
+use Box\Spout\Writer\Style\Color;
+
+$style = (new StyleBuilder())
+           ->setFontBold()
+           ->setFontSize(15)
+           ->setFontColor(Color::BLUE)
+           ->setShouldWrapText()
+           ->build();
+
+$writer = WriterFactory::create(Type::XLSX);
+$writer->openToFile($filePath);
+
+$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row
+$writer->addRow($otherSingleRow); // no style will be applied
+$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows
+
+$writer->close();
+```
+
+Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones:
+
+Category  | Property      | API
+----------|---------------|---------------------------------------
+Font      | Bold          | `StyleBuilder::setFontBold()`
+          | Italic        | `StyleBuilder::setFontItalic()`
+          | Underline     | `StyleBuilder::setFontUnderline()`
+          | Strikethrough | `StyleBuilder::setFontStrikethrough()`
+          | Font name     | `StyleBuilder::setFontName('Arial')`
+          | Font size     | `StyleBuilder::setFontSize(14)`
+          | Font color    | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
+Alignment | Wrap text     | `StyleBuilder::setShouldWrapText()`
+
+
+#### New sheet creation
+
+It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
+```php
+use Box\Spout\Writer\WriterFactory;
+use Box\Spout\Common\Type;
+
+$writer = WriterFactory::create(Type::ODS);
+$writer->setShouldCreateNewSheetsAutomatically(true); // default value
+$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached
+```
+
+#### Using custom temporary folder
+
+Processing XLSX and ODS files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer:
+```php
+use Box\Spout\Writer\WriterFactory;
+use Box\Spout\Common\Type;
+
+$writer = WriterFactory::create(Type::XLSX);
+$writer->setTempFolder($customTempFolderPath);
+```
+
+#### Strings storage (XLSX writer)
+
+XLSX files support different ways to store the string values:
+* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored)
+* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process
+
+In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode.
+```php
+use Box\Spout\Writer\WriterFactory;
+use Box\Spout\Common\Type;
+
+$writer = WriterFactory::create(Type::XLSX);
+$writer->setShouldUseInlineStrings(true); // default (and recommended) value
+$writer->setShouldUseInlineStrings(false); // will use shared strings
+```
+
+> ##### Note on Apple Numbers and iOS support
+>
+> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
+
+
+### Playing with sheets
+
+When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet:
+```php
+$firstSheet = $writer->getCurrentSheet();
+$writer->addRow($rowForSheet1); // writes the row to the first sheet
+
+$newSheet = $writer->addNewSheetAndMakeItCurrent();
+$writer->addRow($rowForSheet2); // writes the row to the new sheet
+
+$writer->setCurrentSheet($firstSheet);
+$writer->addRow($anotherRowForSheet1); // append the row to the first sheet
+```
+
+It is also possible to retrieve all the sheets currently created:
+```php
+$sheets = $writer->getSheets();
+```
+
+If you rely on the sheet's name in your application, you can access it and customize it this way:
+```php
+// Accessing the sheet name when reading
+foreach ($reader->getSheetIterator() as $sheet) {
+    $sheetName = $sheet->getName();
+}
+
+// Accessing the sheet name when writing
+$sheet = $writer->getCurrentSheet();
+$sheetName = $sheet->getName();
+
+//