Merge branch 'wip-MDL-32393-m23' of git://github.com/samhemelryk/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 16 Apr 2012 05:29:04 +0000 (13:29 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 16 Apr 2012 05:29:04 +0000 (13:29 +0800)
282 files changed:
admin/tool/phpunit/cli/init.bat [new file with mode: 0644]
admin/tool/phpunit/cli/init.php [new file with mode: 0644]
admin/tool/phpunit/cli/init.sh [new file with mode: 0755]
admin/tool/phpunit/cli/util.php
admin/tool/phpunit/index.php
admin/tool/phpunit/settings.php
admin/tool/phpunit/version.php
admin/tool/phpunit/webrunner.php [new file with mode: 0644]
admin/tool/qeupgradehelper/README.txt
admin/tool/qeupgradehelper/cli/convert.php [new file with mode: 0644]
admin/tool/qeupgradehelper/locallib.php
backup/moodle2/backup_activity_task.class.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_activity_task.class.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/util/checks/backup_check.class.php
backup/util/factories/tests/factories_test.php
backup/util/loggers/simpletest/testlogger.php
backup/util/loggers/tests/logger_test.php
blocks/online_users/tests/generator/lib.php [new file with mode: 0644]
blocks/online_users/tests/generator_test.php [new file with mode: 0644]
blog/locallib.php
blog/tests/bloglib_test.php
config-dist.php
grade/report/grader/lib.php
index.php
install/lang/de/error.php
install/lang/de_comm/langconfig.php [new file with mode: 0644]
lang/en/backup.php
lang/en/question.php
lib/adminlib.php
lib/clilib.php
lib/cronlib.php
lib/ddl/mssql_sql_generator.php
lib/ddl/simpletest/testddl.php
lib/ddl/tests/ddl_test.php
lib/dml/database_column_info.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/simpletest/testdml.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/enrollib.php
lib/externallib.php
lib/grade/tests/fixtures/lib.php [new file with mode: 0644]
lib/grade/tests/grade_category_test.php [new file with mode: 0644]
lib/grade/tests/grade_grade_test.php [new file with mode: 0644]
lib/grade/tests/grade_item_test.php [new file with mode: 0644]
lib/grade/tests/grade_outcome_test.php [new file with mode: 0644]
lib/grade/tests/grade_scale_test.php [new file with mode: 0644]
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/pdflib.php
lib/phpunit/bootstrap.php
lib/phpunit/bootstraplib.php [new file with mode: 0644]
lib/phpunit/generatorlib.php
lib/phpunit/lib.php
lib/phpunit/phpunit.xsd [new file with mode: 0644]
lib/phpunit/readme.md
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/tcpdf/2dbarcodes.php
lib/tcpdf/CHANGELOG.TXT
lib/tcpdf/LICENSE.TXT
lib/tcpdf/README.TXT
lib/tcpdf/barcodes.php
lib/tcpdf/config/tcpdf_config.php
lib/tcpdf/datamatrix.php
lib/tcpdf/encodings_maps.php
lib/tcpdf/fonts/freemono.ctg.z
lib/tcpdf/fonts/freemono.php
lib/tcpdf/fonts/freemonob.ctg.z
lib/tcpdf/fonts/freemonob.php
lib/tcpdf/fonts/freemonobi.ctg.z
lib/tcpdf/fonts/freemonobi.php
lib/tcpdf/fonts/freemonoi.ctg.z
lib/tcpdf/fonts/freemonoi.php
lib/tcpdf/fonts/freesans.ctg.z
lib/tcpdf/fonts/freesans.php
lib/tcpdf/fonts/freesansb.ctg.z
lib/tcpdf/fonts/freesansb.php
lib/tcpdf/fonts/freesansbi.ctg.z
lib/tcpdf/fonts/freesansbi.php
lib/tcpdf/fonts/freesansi.ctg.z
lib/tcpdf/fonts/freesansi.php
lib/tcpdf/fonts/freeserif.ctg.z
lib/tcpdf/fonts/freeserif.php
lib/tcpdf/fonts/freeserifb.ctg.z
lib/tcpdf/fonts/freeserifb.php
lib/tcpdf/fonts/freeserifbi.ctg.z
lib/tcpdf/fonts/freeserifbi.php
lib/tcpdf/fonts/freeserifi.ctg.z
lib/tcpdf/fonts/freeserifi.php
lib/tcpdf/htmlcolors.php
lib/tcpdf/pdf417.php
lib/tcpdf/qrcode.php
lib/tcpdf/readme_moodle.txt
lib/tcpdf/sRGB.icc [new file with mode: 0644]
lib/tcpdf/spotcolors.php
lib/tcpdf/tcpdf.php
lib/tcpdf/tcpdf_filters.php
lib/tcpdf/tcpdf_parser.php
lib/tcpdf/unicode_data.php
lib/tests/fixtures/sample_dataset.csv [new file with mode: 0644]
lib/tests/fixtures/sample_dataset.xml [new file with mode: 0644]
lib/tests/phpunit_test.php
lib/thirdpartylibs.xml
lib/upgradelib.php
mod/assignment/tests/generator/lib.php [new file with mode: 0644]
mod/assignment/tests/generator_test.php [new file with mode: 0644]
mod/data/tests/fixtures/test_data_content.csv [new file with mode: 0644]
mod/data/tests/fixtures/test_data_fields.csv [new file with mode: 0644]
mod/data/tests/fixtures/test_data_records.csv [new file with mode: 0644]
mod/data/tests/generator/lib.php [new file with mode: 0644]
mod/data/tests/generator_test.php [new file with mode: 0644]
mod/data/tests/search_test.php [new file with mode: 0644]
mod/feedback/item/label/label_form.php
mod/feedback/item/label/lib.php
mod/feedback/show_nonrespondents.php
mod/forum/tests/generator/lib.php [new file with mode: 0644]
mod/forum/tests/generator_test.php [new file with mode: 0644]
mod/page/tests/generator/lib.php [new file with mode: 0644]
mod/page/tests/generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/delaybetweenattempts/rule.php
mod/quiz/editlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/mod_form.php
mod/quiz/report/grading/gradingsettings_form.php
mod/quiz/report/grading/lang/en/quiz_grading.php
mod/quiz/report/grading/report.php
mod/quiz/report/overview/overview_table.php
mod/quiz/report/overview/report.php
mod/quiz/report/responses/report.php
mod/quiz/report/responses/responses_table.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/styles.css
mod/scorm/player.js
mod/workshop/allocation.php
mod/workshop/allocation/lib.php
mod/workshop/allocation/manual/lib.php
mod/workshop/allocation/random/lib.php
mod/workshop/allocation/random/settings_form.php
mod/workshop/allocation/random/version.php
mod/workshop/allocation/scheduled/db/install.xml [new file with mode: 0644]
mod/workshop/allocation/scheduled/lang/en/workshopallocation_scheduled.php [new file with mode: 0644]
mod/workshop/allocation/scheduled/lib.php [new file with mode: 0644]
mod/workshop/allocation/scheduled/settings_form.php [new file with mode: 0644]
mod/workshop/allocation/scheduled/version.php [new file with mode: 0644]
mod/workshop/db/install.xml
mod/workshop/db/upgrade.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/mod_form.php
mod/workshop/renderer.php
mod/workshop/version.php
phpunit.xml.dist
pix/i/scheduled.png [new file with mode: 0644]
question/behaviour/adaptive/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/adaptivenopenalty/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/deferredcbm/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/deferredfeedback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/immediatecbm/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/immediatefeedback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/informationitem/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/interactive/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/interactivecountback/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/manualgraded/tests/walkthrough_test.php [new file with mode: 0644]
question/behaviour/missing/tests/missingbehaviour_test.php [new file with mode: 0644]
question/engine/questionusage.php
question/engine/tests/datalib_test.php [new file with mode: 0644]
question/engine/tests/helpers.php [new file with mode: 0644]
question/engine/tests/questionattempt_test.php [new file with mode: 0644]
question/engine/tests/questionattemptiterator_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstep_test.php [new file with mode: 0644]
question/engine/tests/questionattemptstepiterator_test.php [new file with mode: 0644]
question/engine/tests/questionbank_test.php [new file with mode: 0644]
question/engine/tests/questioncbm_test.php [new file with mode: 0644]
question/engine/tests/questionengine_test.php [new file with mode: 0644]
question/engine/tests/questionstate_test.php [new file with mode: 0644]
question/engine/tests/questionusagebyactivity_test.php [new file with mode: 0644]
question/engine/tests/questionutils_test.php [new file with mode: 0644]
question/engine/tests/unitofwork_test.php [new file with mode: 0644]
question/engine/upgrade/tests/helper.php [new file with mode: 0644]
question/engine/upgrade/upgradelib.php
question/format/gift/tests/fixtures/questions.gift.txt [new file with mode: 0644]
question/format/gift/tests/giftformat_test.php [new file with mode: 0644]
question/format/xml/tests/xmlformat_test.php [new file with mode: 0644]
question/tests/importexport_test.php [new file with mode: 0644]
question/type/calculated/tests/helper.php [new file with mode: 0644]
question/type/calculated/tests/question_test.php [new file with mode: 0644]
question/type/calculated/tests/questiontype_test.php [new file with mode: 0644]
question/type/calculated/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculated/tests/variablesubstituter_test.php [new file with mode: 0644]
question/type/calculated/tests/walkthrough_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/helper.php [new file with mode: 0644]
question/type/calculatedmulti/tests/question_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculatedmulti/tests/walkthrough_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/helper.php [new file with mode: 0644]
question/type/calculatedsimple/tests/question_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/calculatedsimple/tests/walkthrough_test.php [new file with mode: 0644]
question/type/description/tests/helper.php [new file with mode: 0644]
question/type/description/tests/questiontype_test.php [new file with mode: 0644]
question/type/description/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/description/tests/walkthrough_test.php [new file with mode: 0644]
question/type/edit_question_form.php
question/type/essay/questiontype.php
question/type/essay/tests/question_test.php [new file with mode: 0644]
question/type/essay/tests/questiontype_test.php [new file with mode: 0644]
question/type/essay/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/match/tests/question_test.php [new file with mode: 0644]
question/type/match/tests/questiontype_test.php [new file with mode: 0644]
question/type/match/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/match/tests/walkthrough_test.php [new file with mode: 0644]
question/type/missingtype/tests/missingtype_test.php [new file with mode: 0644]
question/type/multianswer/tests/helper.php [new file with mode: 0644]
question/type/multianswer/tests/question_test.php [new file with mode: 0644]
question/type/multianswer/tests/questiontype_test.php [new file with mode: 0644]
question/type/multianswer/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/multianswer/tests/walkthrough_test.php [new file with mode: 0644]
question/type/multichoice/questiontype.php
question/type/multichoice/tests/question_test.php [new file with mode: 0644]
question/type/multichoice/tests/questiontype_test.php [new file with mode: 0644]
question/type/multichoice/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/multichoice/tests/walkthrough_test.php [new file with mode: 0644]
question/type/numerical/question.php
question/type/numerical/questiontype.php
question/type/numerical/tests/answer_test.php [new file with mode: 0644]
question/type/numerical/tests/answerprocessor_test.php [new file with mode: 0644]
question/type/numerical/tests/form_test.php [new file with mode: 0644]
question/type/numerical/tests/helper.php [new file with mode: 0644]
question/type/numerical/tests/question_test.php [new file with mode: 0644]
question/type/numerical/tests/questiontype_test.php [new file with mode: 0644]
question/type/numerical/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/numerical/tests/walkthrough_test.php [new file with mode: 0644]
question/type/questiontypebase.php
question/type/random/tests/questiontype_test.php [new file with mode: 0644]
question/type/random/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/shortanswer/tests/helper.php [new file with mode: 0644]
question/type/shortanswer/tests/question_test.php [new file with mode: 0644]
question/type/shortanswer/tests/questiontype_test.php [new file with mode: 0644]
question/type/shortanswer/tests/tupgradelibnewqe_test.php [new file with mode: 0644]
question/type/tests/questionbase_test.php [new file with mode: 0644]
question/type/tests/questiontype_test.php [new file with mode: 0644]
question/type/truefalse/questiontype.php
question/type/truefalse/tests/helper.php [new file with mode: 0644]
question/type/truefalse/tests/question_test.php [new file with mode: 0644]
question/type/truefalse/tests/questiontype_test.php [new file with mode: 0644]
question/type/truefalse/tests/upgradelibnewqe_test.php [new file with mode: 0644]
question/type/truefalse/tests/walkthrough_test.php [new file with mode: 0644]
question/type/upgrade.txt
rating/index.php
repository/lib.php
theme/afterburner/style/afterburner_styles.css
theme/base/style/course.css
theme/formal_white/config.php
theme/formal_white/lang/en/theme_formal_white.php
theme/formal_white/lib.php
theme/formal_white/settings.php
theme/formal_white/style/core.css
theme/formal_white/style/course.css
theme/formal_white/style/formal_white.css
theme/formal_white/style/menu.css
theme/formal_white/style/pagelayout.css
theme/formal_white/style/quiz.css
theme/formal_white/version.php
version.php

diff --git a/admin/tool/phpunit/cli/init.bat b/admin/tool/phpunit/cli/init.bat
new file mode 100644 (file)
index 0000000..d57029f
--- /dev/null
@@ -0,0 +1,22 @@
+@ECHO OFF
+ECHO Initialising Moodle PHPUnit test environment...
+
+CALL php %~dp0\util.php --diag > NUL 2>&1
+
+IF ERRORLEVEL 133 GOTO drop
+IF ERRORLEVEL 132 GOTO install
+IF ERRORLEVEL 1 GOTO unknown
+GOTO done
+
+:drop
+CALL php %~dp0\util.php --drop
+IF ERRORLEVEL 1 GOTO done
+
+:install
+CALL php %~dp0\util.php --install
+GOTO done
+
+:unknown
+CALL php %~dp0\util.php --diag
+
+:done
diff --git a/admin/tool/phpunit/cli/init.php b/admin/tool/phpunit/cli/init.php
new file mode 100644 (file)
index 0000000..f091a9f
--- /dev/null
@@ -0,0 +1,66 @@
+<?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/>.
+
+/**
+ * All in one init script - PHP version.
+ *
+ * @package    tool_phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+if (isset($_SERVER['REMOTE_ADDR'])) {
+    die; // no access from web!
+}
+
+require_once(__DIR__.'/../../../../lib/clilib.php');
+require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
+
+echo "Initialising Moodle PHPUnit test environment...\n";
+
+$output = null;
+exec('php --version', $output, $code);
+if ($code != 0) {
+    phpunit_bootstrap_error(1, 'Can not execute \'php\' binary.');
+}
+
+chdir(__DIR__);
+$output = null;
+exec("php util.php --diag", $output, $code);
+if ($code == 0) {
+    // everything is ready
+
+} else if ($code == 132) {
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else if ($code == 133) {
+    passthru("php util.php --drop", $code);
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else {
+    echo implode("\n", $output)."\n";
+    exit($code);
+}
+
+passthru("php util.php --buildconfig", $code);
+
+exit(0);
diff --git a/admin/tool/phpunit/cli/init.sh b/admin/tool/phpunit/cli/init.sh
new file mode 100755 (executable)
index 0000000..b3616ee
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+SOURCE="${BASH_SOURCE[0]}"
+while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done
+CLIDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
+
+UTIL="$CLIDIR/util.php"
+
+echo "Initialising Moodle PHPUnit test environment..."
+
+DIGERROR=`php $UTIL --diag`
+DIAG=$?
+if [ $DIAG -eq 132 ] ; then
+    php $UTIL --install
+else
+    if [ $DIAG -eq 133 ] ; then
+        php $UTIL --drop
+        RESULT=$?
+        if [ $RESULT -gt 0 ] ; then
+            exit $RESULT
+        fi
+        php $UTIL --install
+    else
+        if [ $DIAG -gt 0 ] ; then
+            echo $DIGERROR
+            exit $DIAG
+        fi
+    fi
+fi
+
+php $UTIL --buildconfig
index 18a08db..0ab46c0 100644 (file)
  * Exit codes:
  *  0   - success
  *  1   - general error
- *  130 - coding error
+ *  130 - missing PHPUnit library error
  *  131 - configuration problem
+ *  132 - install new test database
  *  133 - drop existing data before installing
+ *  134 - can not create main phpunit.xml
  *
  * @package    tool_phpunit
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define('PHPUNIT_UTIL', true);
+if (isset($_SERVER['REMOTE_ADDR'])) {
+    die; // no access from web!
+}
 
-require(__DIR__ . '/../../../../lib/phpunit/bootstrap.php');
-require_once($CFG->libdir.'/phpunit/lib.php');
-require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/upgradelib.php');
-require_once($CFG->libdir.'/clilib.php');
-require_once($CFG->libdir.'/pluginlib.php');
-require_once($CFG->libdir.'/installlib.php');
+require_once(__DIR__.'/../../../../lib/clilib.php');
+require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 
 // now get cli options
 list($options, $unrecognized) = cli_get_params(
@@ -45,6 +44,9 @@ list($options, $unrecognized) = cli_get_params(
         'drop'        => false,
         'install'     => false,
         'buildconfig' => false,
+        'diag'        => false,
+        'phpunitdir'  => false,
+        'run'         => false,
         'help'        => false,
     ),
     array(
@@ -52,37 +54,105 @@ list($options, $unrecognized) = cli_get_params(
     )
 );
 
+if ($options['phpunitdir']) {
+    // nasty skodak's hack for testing of future PHPUnit versions - intentionally not documented
+    if (!file_exists($options['phpunitdir'])) {
+        cli_error('Invalid custom PHPUnit lib location');
+    }
+    $files = scandir($options['phpunitdir']);
+    foreach ($files as $file) {
+        $path = $options['phpunitdir'].'/'.$file;
+        if (!is_dir($path) or strpos($file, '.') === 0) {
+            continue;
+        }
+        ini_set('include_path', $path . PATH_SEPARATOR . ini_get('include_path'));
+    }
+    unset($files);
+    unset($file);
+}
+
+// verify PHPUnit libs are loaded
+if (!@include_once('PHPUnit/Autoload.php')) {
+    phpunit_bootstrap_error(130);
+}
+
+if (!@include_once('PHPUnit/Extensions/Database/Autoload.php')) {
+    phpunit_bootstrap_error(130);
+}
+
+if ($options['run']) {
+    unset($options);
+    unset($unrecognized);
+
+    foreach ($_SERVER['argv'] as $k=>$v) {
+        if (strpos($v, '--run') === 0 or strpos($v, '--phpunitdir') === 0) {
+            unset($_SERVER['argv'][$k]);
+            $_SERVER['argc'] = $_SERVER['argc'] - 1;
+        }
+    }
+    $_SERVER['argv'] = array_values($_SERVER['argv']);
+    PHPUnit_TextUI_Command::main();
+    exit(0);
+}
+
+define('PHPUNIT_UTIL', true);
+
+require(__DIR__ . '/../../../../lib/phpunit/bootstrap.php');
+
+// from now on this is a regular moodle CLI_SCRIPT
+
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/upgradelib.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->libdir.'/pluginlib.php');
+require_once($CFG->libdir.'/installlib.php');
+
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
     cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
 }
 
+$diag = $options['diag'];
 $drop = $options['drop'];
 $install = $options['install'];
 $buildconfig = $options['buildconfig'];
 
-if ($options['help'] or (!$drop and !$install and !$buildconfig)) {
+if ($options['help'] or (!$drop and !$install and !$buildconfig and !$diag)) {
     $help = "Various PHPUnit utility functions
 
 Options:
 --drop                Drop database and dataroot
 --install             Install database
 --buildconfig         Build /phpunit.xml from /phpunit.xml.dist that includes suites for all plugins and core
+--diag                Diagnose installation and return error code only
+--run                 Execute PHPUnit tests (alternative for standard phpunit binary)
 
 -h, --help            Print out this help
 
 Example:
-\$/usr/bin/php lib/phpunit/tool.php
+\$/usr/bin/php lib/phpunit/tool.php --install
 ";
     echo $help;
-    die;
+    exit(0);
 }
 
-if ($buildconfig) {
-    phpunit_util::build_config_file();
+if ($diag) {
+    list($errorcode, $message) = phpunit_util::testing_ready_problem();
+    if ($errorcode) {
+        phpunit_bootstrap_error($errorcode, $message);
+    }
     exit(0);
 
+} else if ($buildconfig) {
+    if (phpunit_util::build_config_file()) {
+        exit(0);
+    } else {
+        phpunit_bootstrap_error(134);
+    }
+
 } else if ($drop) {
+    // make sure tests do not run in parallel
+    phpunit_util::acquire_test_lock();
     phpunit_util::drop_site();
     // note: we must stop here because $CFG is messed up and we can not reinstall, sorry
     exit(0);
index 1dbaded..774d4ee 100644 (file)
@@ -22,8 +22,6 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define('NO_OUTPUT_BUFFERING', true);
-
 require(dirname(__FILE__) . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 
@@ -37,4 +35,4 @@ $info = file_get_contents("$CFG->libdir/phpunit/readme.md");
 echo markdown_to_html($info);
 
 echo $OUTPUT->box_end();
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index 8e06b2f..efefd32 100644 (file)
@@ -25,4 +25,8 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$ADMIN->add('development', new admin_externalpage('toolphpunit', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/index.php"));
+if ($hassiteconfig) {
+    $ADMIN->add('development', new admin_externalpage('toolphpunit', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/index.php"));
+    $ADMIN->add('development', new admin_externalpage('toolphpunitwebrunner', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/webrunner.php",
+        'moodle/site:config', true));
+}
index e745026..9e59434 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012030800; // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2012030100; // Requires this Moodle version
+$plugin->version   = 2012040500; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2012040500; // Requires this Moodle version
 $plugin->component = 'tool_phpunit'; // Full name of the plugin (used for diagnostics)
 
diff --git a/admin/tool/phpunit/webrunner.php b/admin/tool/phpunit/webrunner.php
new file mode 100644 (file)
index 0000000..ef51685
--- /dev/null
@@ -0,0 +1,192 @@
+<?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/>.
+
+/**
+ * PHPUnit shell execution wrapper
+ *
+ * @package    tool_phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
+require(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$testpath  = optional_param('testpath', '', PARAM_PATH);
+$testclass = optional_param('testclass', '', PARAM_ALPHANUMEXT);
+$execute   = optional_param('execute', 0, PARAM_BOOL);
+
+navigation_node::override_active_url(new moodle_url('/admin/tool/phpunit/index.php'));
+admin_externalpage_setup('toolphpunitwebrunner');
+
+if (!debugging('', DEBUG_DEVELOPER)) {
+    error('Not available on production sites, sorry.');
+}
+
+set_time_limit(60*30);
+
+$oldcwd = getcwd();
+$code = 0;
+
+if (!isset($CFG->phpunit_dataroot) or !isset($CFG->phpunit_prefix)) {
+    tool_phpunit_problem('Missing $CFG->phpunit_dataroot or $CFG->phpunit_prefix, can not execute tests.');
+}
+if (!file_exists($CFG->phpunit_dataroot)) {
+    mkdir($CFG->phpunit_dataroot, 02777, true);
+}
+if (!is_writable($CFG->phpunit_dataroot)) {
+    tool_phpunit_problem('$CFG->phpunit_dataroot in not writable, can not execute tests.');
+}
+$output = null;
+exec('php --version', $output, $code);
+if ($code != 0) {
+    tool_phpunit_problem('Can not execute \'php\' binary.');
+}
+
+if ($execute) {
+    require_sesskey();
+
+    chdir($CFG->dirroot);
+    $output = null;
+    exec("php $CFG->admin/tool/phpunit/cli/util.php --diag", $output, $code);
+    if ($code == 0) {
+        // everything is ready
+
+    } else if ($code == 132) {
+        tool_phpunit_header();
+        echo $OUTPUT->box_start('generalbox');
+        echo '<pre>';
+        echo "Initialising test database:\n\n";
+        chdir($CFG->dirroot);
+        ignore_user_abort(true);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --install", $code);
+        chdir($oldcwd);
+        echo '</pre>';
+        echo $OUTPUT->box_end();
+        if ($code != 0) {
+            tool_phpunit_problem('Can not initialize database');
+        }
+        $CFG->debug = 0; // no pesky redirect warning, we really want to redirect
+        redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
+        echo $OUTPUT->footer();
+        die();
+
+    } else if ($code == 133) {
+        tool_phpunit_header();
+        echo $OUTPUT->box_start('generalbox');
+        echo '<pre>';
+        echo "Reinitialising test database:\n\n";
+        chdir($CFG->dirroot);
+        ignore_user_abort(true);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --drop", $code);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --install", $code);
+        chdir($oldcwd);
+        echo '</pre>';
+        echo $OUTPUT->box_end();
+        if ($code != 0) {
+            tool_phpunit_problem('Can not initialize database');
+        }
+        $CFG->debug = 0; // no pesky redirect warning, we really want to redirect
+        redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
+        die();
+
+    } else {
+        tool_phpunit_header();
+        echo $OUTPUT->box_start('generalbox');
+        echo '<pre>';
+        echo "Error: $code\n\n";
+        echo implode("\n", $output);
+        echo '</pre>';
+        echo $OUTPUT->box_end();
+        tool_phpunit_problem('Can not execute tests');
+        die();
+    }
+
+    tool_phpunit_header();
+    echo $OUTPUT->box_start('generalbox');
+    echo '<pre>';
+
+    // use the dataroot file
+    $configdir = "$CFG->phpunit_dataroot/phpunit/webrunner.xml";
+    if (!file_exists($configdir)) {
+        passthru("php $CFG->admin/tool/phpunit/cli/util.php --buildconfig", $code);
+        if ($code != 0) {
+            tool_phpunit_problem('Can not create configuration file');
+        }
+    }
+    $configdir = escapeshellarg($configdir);
+    // no cleanup of path - this is tricky because we can not use escapeshellarg and friends for escaping,
+    // this is from admin user so PARAM_PATH must be enough
+    chdir($CFG->dirroot);
+    passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $testclass $testpath", $code);
+    chdir($oldcwd);
+
+    echo '</pre>';
+    echo $OUTPUT->box_end();
+
+} else {
+    tool_phpunit_header();
+}
+
+echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
+echo '<form method="get" action="webrunner.php">';
+echo '<fieldset class="invisiblefieldset">';
+echo '<label for="testpath">Test one file</label> ';
+echo '<input type="text" id="testpath" name="testpath" value="'.s($testpath).'" size="50" /> (all test cases from webrunner.xml if empty)';
+echo '</p>';
+echo '<label for="testclass">Class name</label> ';
+echo '<input type="text" id="testclass" name="testclass" value="'.s($testclass).'" size="50" /> (first class in file if empty)';
+echo '</p>';
+echo '<input type="submit" value="Run" />';
+echo '<input type="hidden" name="execute" value="1" />';
+echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
+echo '</fieldset>';
+echo '</form>';
+echo $OUTPUT->box_end();
+echo $OUTPUT->footer();
+die;
+
+
+
+//========================================
+
+/**
+ * Print headers and experimental warning
+ * @return void
+ */
+function tool_phpunit_header() {
+    global $OUTPUT;
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('pluginname', 'tool_phpunit'));
+    echo $OUTPUT->box('EXPERIMENTAL: it is recommended to execute PHPUnit tests and init scripts only from command line.', array('generalbox'));
+}
+
+/**
+ * Called when PHPUnit can not execute.
+ * @param string $message
+ * @return void
+ */
+function tool_phpunit_problem($message) {
+    global $PAGE;
+    if (!$PAGE->headerprinted) {
+        tool_phpunit_header();
+    }
+    notice($message, new moodle_url('/admin/tool/phpunit/'));
+}
index ce0825b..b9ce983 100644 (file)
@@ -5,7 +5,7 @@ With a lot of question attempts, doing the whole conversion on upgrade is very
 slow. The plugin can help with that in various ways.
 
 
-1. It provies a report of how much data there is to upgrade.
+1. It provides a report of how much data there is to upgrade.
 
 2. It can extract test-cases from the database. This can help you report bugs
 in the upgrade process to the developers.
@@ -35,3 +35,9 @@ subsequently been modified) so you can re-upgrade them. This may allow you to
 recover from a buggy upgrade.
 
 9. Finally, you can still use the extract test-cases script to help report bugs.
+
+
+Manual upgrades can be processed via the web interface or the command line tool
+cliupgrade.php. To run cliupgrade.php, use a command similar to:
+sudo -u www-data /usr/bin/php admin/tool/qeupgradehelper/cli/convert.php -h
+The -h flag will show the options for running the tool.
\ No newline at end of file
diff --git a/admin/tool/qeupgradehelper/cli/convert.php b/admin/tool/qeupgradehelper/cli/convert.php
new file mode 100644 (file)
index 0000000..779442e
--- /dev/null
@@ -0,0 +1,129 @@
+<?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/>.
+
+/**
+ * Script to allow upgrading of quizzes with attempts that were previously
+ * skipped.
+ *
+ * @package    tool_qeupgradehelper
+ * @copyright  2012 Eric Merrill, Oakland Unversity
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require_once(dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/config.php');
+require_once(dirname(dirname(__FILE__)) . '/locallib.php');
+require_once(dirname(dirname(__FILE__)) . '/lib.php');
+require_once($CFG->libdir.'/clilib.php');      // CLI only functions.
+
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array('quiz'=>false, 'timelimit'=>false, 'countlimit'=>false, 'help'=>false),
+                                               array('c'=>'countlimit', 't'=>'timelimit', 'h'=>'help'));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+
+if ($options['help']) {
+    $help =
+"Question engine upgrade helper CLI tool.
+Will upgrade all remaining question attempts if no options are specified.
+
+Options:
+-c, --countlimit=<n>    Process n number of quizzes then exit
+-t, --timelimit=<n>     Process quizzes for n number of seconds, then exit. A quiz
+                        currently in progress will not be interrupted.
+--quiz=<quizid>         Process quiz quizid only
+-h, --help              Print out this help
+
+countlimit and timelimit can be used together. First one to trigger will stop execution.
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/tool/qeupgradehelper/cliupgrade.php
+";
+
+    echo $help;
+    die;
+}
+
+
+
+
+if (!tool_qeupgradehelper_is_upgraded()) {
+    mtrace('qeupgradehelper: site not yet upgraded. Doing nothing.');
+    return;
+}
+
+require_once(dirname(dirname(__FILE__)) . '/afterupgradelib.php');
+
+
+$starttime = time();
+
+// Setup the stop time.
+if ($options['timelimit']) {
+    $stoptime = time() + $options['timelimit'];
+} else {
+    $stoptime = false;
+}
+
+// If we are doing a quiz id, limit to one.
+if ($options['quiz']) {
+    $options['countlimit'] = 1;
+}
+
+$count = 0;
+
+
+mtrace('qeupgradehelper: processing ...');
+
+/* This while statement does a few things
+ * Basically if an option is set to false, then that subsection will return
+ * true, and will short circuit the test condition for that option, and always
+ * being true. Both options are anded together, so either one can trigger to stop.
+ */
+while ((!$stoptime || (time() < $stoptime)) && (!$options['countlimit'] || ($count < $options['countlimit']))) {
+    if ($options['quiz']) {
+        $quizid = $options['quiz'];
+    } else {
+        $quiz = tool_qeupgradehelper_get_quiz_for_upgrade();
+        if (!$quiz) {
+            mtrace('qeupgradehelper: No more quizzes to process.');
+            break; // No more to do.
+        }
+
+        $quizid = $quiz->id;
+    }
+    $quizsummary = tool_qeupgradehelper_get_quiz($quizid);
+    if ($quizsummary) {
+        mtrace('  starting upgrade of attempts at quiz ' . $quizid);
+        $upgrader = new tool_qeupgradehelper_attempt_upgrader(
+                $quizsummary->id, $quizsummary->numtoconvert);
+        $upgrader->convert_all_quiz_attempts();
+        mtrace('  upgrade of quiz ' . $quizid . ' complete.');
+    } else {
+        mtrace('quiz ' . $quizid . ' not found or already upgraded.');
+    }
+
+    $count++;
+}
+
+
+mtrace('qeupgradehelper: Done. Processed '.$count.' quizes in '.(time()-$starttime).' seconds');
+return;
index 62cfca6..eda56a8 100644 (file)
@@ -669,5 +669,5 @@ function tool_qeupgradehelper_get_quiz_for_upgrade() {
             JOIN {course} c ON c.id = quiz.course
             WHERE quiza.preview = 0 AND quiza.needsupgradetonewqe = 1
             GROUP BY quiz.id, quiz.name, c.shortname, c.id
-            ORDER BY quiza.timemodified DESC", array(), IGNORE_MULTIPLE);
+            ORDER BY MAX(quiza.timemodified) DESC", array(), IGNORE_MULTIPLE);
 }
index 360dd63..fef94cc 100644 (file)
@@ -167,6 +167,11 @@ abstract class backup_activity_task extends backup_task {
             $this->add_step(new backup_activity_logs_structure_step('activity_logs', 'logs.xml'));
         }
 
+        // Generate the calendar events file (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new backup_calendarevents_structure_step('activity_calendar', 'calendar.xml'));
+        }
+
         // Fetch all the activity grade items and put them to backup_ids
         $this->add_step(new backup_activity_grade_items_to_ids('fetch_activity_grade_items'));
 
index 8c35274..6cc262b 100644 (file)
@@ -106,6 +106,11 @@ class backup_course_task extends backup_task {
             $this->add_step(new backup_comments_structure_step('course_comments', 'comments.xml'));
         }
 
+        // Generate the calender events file (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new backup_calendarevents_structure_step('course_calendar', 'calendar.xml'));
+        }
+
         // Generate the logs file (conditionally)
         if ($this->get_setting_value('logs')) {
             $this->add_step(new backup_course_logs_structure_step('course_logs', 'logs.xml'));
index 861be13..3bd46cf 100644 (file)
@@ -119,6 +119,12 @@ class backup_root_task extends backup_task {
         $this->add_setting($comments);
         $users->add_dependency($comments);
 
+        // Define calendar events (dependent of users)
+        $events = new backup_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, true);
+        $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup')));
+        $this->add_setting($events);
+        $users->add_dependency($events);
+
         // Define completion (dependent of users)
         $completion = new backup_userscompletion_setting('userscompletion', base_setting::IS_BOOLEAN, true);
         $completion->set_ui(new backup_setting_ui_checkbox($completion, get_string('rootsettinguserscompletion', 'backup')));
index 74914aa..ddd6aa2 100644 (file)
@@ -101,6 +101,13 @@ class backup_logs_setting extends backup_anonymize_setting {}
  */
 class backup_comments_setting extends backup_anonymize_setting {}
 
+/**
+ * root setting to control if backup will include
+ * calender events or no (any level), depends of @backup_users_setting
+ * exactly in the same way than @backup_anonymize_setting so we extend from it
+ */
+class backup_calendarevents_setting extends backup_anonymize_setting {}
+
 /**
  * root setting to control if backup will include
  * users completion data or no (any level), depends of @backup_users_setting
index e22ff35..47d4040 100644 (file)
@@ -762,6 +762,48 @@ class backup_comments_structure_step extends backup_structure_step {
     }
 }
 
+/**
+ * structure step in charge of constructing the calender.xml file for all the events found
+ * in a given context
+ */
+class backup_calendarevents_structure_step extends backup_structure_step {
+
+    protected function define_structure() {
+
+        // Define each element separated
+
+        $events = new backup_nested_element('events');
+
+        $event = new backup_nested_element('event', array('id'), array(
+                'name', 'description', 'format', 'courseid', 'groupid', 'userid',
+                'repeatid', 'modulename', 'instance', 'eventtype', 'timestart',
+                'timeduration', 'visible', 'uuid', 'sequence', 'timemodified'));
+
+        // Build the tree
+        $events->add_child($event);
+
+        // Define sources
+        if ($this->name == 'course_calendar') {
+            $calendar_items_sql ="SELECT * FROM {event}
+                        WHERE courseid = :courseid
+                        AND (eventtype = 'course' OR eventtype = 'group')";
+            $calendar_items_params = array('courseid'=>backup::VAR_COURSEID);
+            $event->set_source_sql($calendar_items_sql, $calendar_items_params);
+        } else {
+            $event->set_source_table('event', array('courseid' => backup::VAR_COURSEID, 'instance' => backup::VAR_ACTIVITYID, 'modulename' => backup::VAR_MODNAME));
+        }
+
+        // Define id annotations
+
+        $event->annotate_ids('user', 'userid');
+        $event->annotate_ids('group', 'groupid');
+        $event->annotate_files('calendar', 'event_description', 'id');
+
+        // Return the root element (events)
+        return $events;
+    }
+}
+
 /**
  * structure step in charge of constructing the gradebook.xml file for all the gradebook config in the course
  * NOTE: the backup of the grade items themselves is handled by backup_activity_grades_structure_step
index 4872fc0..6917b33 100644 (file)
@@ -152,6 +152,11 @@ abstract class restore_activity_task extends restore_task {
             $this->add_step(new restore_comments_structure_step('activity_comments', 'comments.xml'));
         }
 
+        // Calendar events (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new restore_calendarevents_structure_step('activity_calendar', 'calendar.xml'));
+        }
+
         // Grades (module-related, rest of gradebook is restored later if possible: cats, calculations...)
         $this->add_step(new restore_activity_grades_structure_step('activity_grades', 'grades.xml'));
 
index 87828b4..97a2376 100644 (file)
@@ -89,6 +89,11 @@ class restore_course_task extends restore_task {
             $this->add_step(new restore_comments_structure_step('course_comments', 'comments.xml'));
         }
 
+        // Calendar events (conditionally)
+        if ($this->get_setting_value('calendarevents')) {
+            $this->add_step(new restore_calendarevents_structure_step('course_calendar', 'calendar.xml'));
+        }
+
         // At the end, mark it as built
         $this->built = true;
     }
index 319fe08..6917f1c 100644 (file)
@@ -174,6 +174,19 @@ class restore_root_task extends restore_task {
         $this->add_setting($comments);
         $users->add_dependency($comments);
 
+        // Define Calendar events (dependent of users)
+        $defaultvalue = false;                      // Safer default
+        $changeable = false;
+        if (isset($rootsettings['calendarevents']) && $rootsettings['calendarevents']) { // Only enabled when available
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $events = new restore_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, $defaultvalue);
+        $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup')));
+        $events->get_ui()->set_changeable($changeable);
+        $this->add_setting($events);
+        $users->add_dependency($events);
+
         // Define completion (dependent of users)
         $defaultvalue = false;                      // Safer default
         $changeable = false;
index 09f92fe..59c09e1 100644 (file)
@@ -63,6 +63,13 @@ class restore_activities_setting extends restore_generic_setting {}
  */
 class restore_comments_setting extends restore_role_assignments_setting {}
 
+/**
+ * root setting to control if restore will create
+ * events or no, depends of @restore_users_setting
+ * exactly in the same way than @restore_role_assignments_setting so we extend from it
+ */
+class restore_calendarevents_setting extends restore_role_assignments_setting {}
+
 /**
  * root setting to control if restore will create
  * completion info or no, depends of @restore_users_setting
index ae2ae0d..1472012 100644 (file)
@@ -1596,6 +1596,76 @@ class restore_comments_structure_step extends restore_structure_step {
     }
 }
 
+/**
+ * This structure steps restores the calendar events
+ */
+class restore_calendarevents_structure_step extends restore_structure_step {
+
+    protected function define_structure() {
+
+        $paths = array();
+
+        $paths[] = new restore_path_element('calendarevents', '/events/event');
+
+        return $paths;
+    }
+
+    public function process_calendarevents($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $oldid = $data->id;
+        $restorefiles = true; // We'll restore the files
+        // Find the userid and the groupid associated with the event. Return if not found.
+        $data->userid = $this->get_mappingid('user', $data->userid);
+        if ($data->userid === false) {
+            return;
+        }
+        if (!empty($data->groupid)) {
+            $data->groupid = $this->get_mappingid('group', $data->groupid);
+            if ($data->groupid === false) {
+                return;
+            }
+        }
+
+        $params = array(
+                'name'           => $data->name,
+                'description'    => $data->description,
+                'format'         => $data->format,
+                'courseid'       => $this->get_courseid(),
+                'groupid'        => $data->groupid,
+                'userid'         => $data->userid,
+                'repeatid'       => $data->repeatid,
+                'modulename'     => $data->modulename,
+                'eventtype'      => $data->eventtype,
+                'timestart'      => $this->apply_date_offset($data->timestart),
+                'timeduration'   => $data->timeduration,
+                'visible'        => $data->visible,
+                'uuid'           => $data->uuid,
+                'sequence'       => $data->sequence,
+                'timemodified'    => $this->apply_date_offset($data->timemodified));
+        if ($this->name == 'activity_calendar') {
+            $params['instance'] = $this->task->get_activityid();
+        } else {
+            $params['instance'] = 0;
+        }
+        $sql = 'SELECT id FROM {event} WHERE name = ? AND courseid = ? AND
+                repeatid = ? AND modulename = ? AND timestart = ? AND timeduration =?
+                AND ' . $DB->sql_compare_text('description', 255) . ' = ' . $DB->sql_compare_text('?', 255);
+        $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
+        $result = $DB->record_exists_sql($sql, $arg);
+        if (empty($result)) {
+            $newitemid = $DB->insert_record('event', $params);
+            $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
+        }
+
+    }
+    protected function after_execute() {
+        // Add related files
+        $this->add_related_files('calendar', 'event_description', 'event_description');
+    }
+}
+
 class restore_course_completion_structure_step extends restore_structure_step {
 
     /**
index 881c73c..fb7839d 100644 (file)
@@ -168,9 +168,9 @@ abstract class backup_check {
         $hasusercap   = has_capability('moodle/backup:userinfo', $coursectx, $userid);
 
         // If setting is enabled but user lacks permission
-        if (!$hasusercap && $prevvalue) { // If user has not the capability and setting is enabled
+        if (!$hasusercap) { // If user has not the capability
             // Now analyse if we are allowed to apply changes or must stop with exception
-            if (!$apply) { // Cannot apply changes, throw exception
+            if (!$apply && $prevvalue) { // Cannot apply changes and the value is set, throw exception
                 $a = new stdclass();
                 $a->setting = 'users';
                 $a->value = $prevvalue;
@@ -178,7 +178,12 @@ abstract class backup_check {
                 throw new backup_controller_exception('backup_setting_value_wrong_for_capability', $a);
 
             } else { // Can apply changes
-                $userssetting->set_value(false);                              // Set the value to false
+                // If it is already false, we don't want to try and set it again, because if it is
+                // already locked, and exception will occur. The side benifit is if it is true and locked
+                // we will get an exception...
+                if ($prevvalue) {
+                    $userssetting->set_value(false);                              // Set the value to false
+                }
                 $userssetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm
             }
         }
@@ -191,9 +196,9 @@ abstract class backup_check {
         $hasanoncap  = has_capability('moodle/backup:anonymise', $coursectx, $userid);
 
         // If setting is enabled but user lacks permission
-        if (!$hasanoncap && $prevvalue) { // If user has not the capability and setting is enabled
+        if (!$hasanoncap) { // If user has not the capability
             // Now analyse if we are allowed to apply changes or must stop with exception
-            if (!$apply) { // Cannot apply changes, throw exception
+            if (!$apply && $prevvalue) { // Cannot apply changes and the value is set, throw exception
                 $a = new stdclass();
                 $a->setting = 'anonymize';
                 $a->value = $prevvalue;
@@ -201,7 +206,9 @@ abstract class backup_check {
                 throw new backup_controller_exception('backup_setting_value_wrong_for_capability', $a);
 
             } else { // Can apply changes
-                $anonsetting->set_value(false);                              // Set the value to false
+                if ($prevvalue) { // If we try and set it back to false and it has already been locked, error will occur
+                    $anonsetting->set_value(false);                              // Set the value to false
+                }
                 $anonsetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm
             }
         }
index 9e3a482..54dc4e9 100644 (file)
@@ -111,7 +111,7 @@ class backup_factories_testcase extends advanced_testcase {
         $this->assertTrue($logger2 instanceof file_logger);  // 2nd logger is file_logger (output_indented_logger skiped)
 
         // Define extra file logger and instantiate, should be 5th and last logger
-        $CFG->backup_file_logger_extra = '/tmp/test.html';
+        $CFG->backup_file_logger_extra = $CFG->tempdir.'/test.html';
         $CFG->backup_file_logger_level_extra = backup::LOG_NONE;
         $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test');
         $logger2 = $logger1->get_next();
index 9374346..c4f49fe 100644 (file)
@@ -272,6 +272,7 @@ class logger_test extends UnitTestCase {
         unlink($file); // delete file
 
         // Try one html file
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $options = array('depth' => 1);
         $lo = new file_logger(backup::LOG_ERROR, true, true, $file);
@@ -289,6 +290,7 @@ class logger_test extends UnitTestCase {
         unlink($file); // delete file
 
         // Instantiate, write something, force deletion, try to write again
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $lo = new mock_file_logger(backup::LOG_ERROR, true, true, $file);
         $this->assertTrue(file_exists($file));
index 799632f..67f8e74 100644 (file)
@@ -267,6 +267,7 @@ class backup_logger_testcase extends basic_testcase {
         unlink($file); // delete file
 
         // Try one html file
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $options = array('depth' => 1);
         $lo = new file_logger(backup::LOG_ERROR, true, true, $file);
@@ -281,9 +282,11 @@ class backup_logger_testcase extends basic_testcase {
         $this->assertTrue(strpos($fcontents, '[error]') !== false);
         $this->assertTrue(strpos($fcontents, '&nbsp;&nbsp;') !== false);
         $this->assertTrue(substr_count($fcontents , '] ') >= 2);
+        $lo->__destruct(); // closes file handle
         unlink($file); // delete file
 
         // Instantiate, write something, force deletion, try to write again
+        check_dir_exists($CFG->tempdir . '/test');
         $file = $CFG->tempdir . '/test/test_file_logger.html';
         $lo = new mock_file_logger(backup::LOG_ERROR, true, true, $file);
         $this->assertTrue(file_exists($file));
diff --git a/blocks/online_users/tests/generator/lib.php b/blocks/online_users/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..d762f42
--- /dev/null
@@ -0,0 +1,63 @@
+<?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/>.
+
+/**
+ * block_online_users data generator
+ *
+ * @package    block_online_users
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Page module PHPUnit data generator class
+ *
+ * @package    mod_page
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_generator extends phpunit_block_generator {
+
+    /**
+     * Create new block instance
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record with extra cmid field
+     */
+    public function create_instance($record = null, array $options = null) {
+        global $DB, $CFG;
+        require_once("$CFG->dirroot/mod/page/locallib.php");
+
+        $this->instancecount++;
+
+        $record = (object)(array)$record;
+        $options = (array)$options;
+
+        $record = $this->prepare_record($record);
+
+        $id = $DB->insert_record('block_instances', $record);
+        context_block::instance($id);
+
+        $instance = $DB->get_record('block_instances', array('id'=>$id), '*', MUST_EXIST);
+
+        return $instance;
+    }
+}
diff --git a/blocks/online_users/tests/generator_test.php b/blocks/online_users/tests/generator_test.php
new file mode 100644 (file)
index 0000000..1ffcaba
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * PHPUnit data generator tests
+ *
+ * @package    block_online_users
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * PHPUnit data generator testcase
+ *
+ * @package    block_online_users
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_generator_testcase extends advanced_testcase {
+    public function test_generator() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $beforeblocks = $DB->count_records('block_instances');
+        $beforecontexts = $DB->count_records('context');
+
+        /** @var block_online_users_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users');
+        $this->assertInstanceOf('block_online_users_generator', $generator);
+        $this->assertEquals('online_users', $generator->get_blockname());
+
+        $generator->create_instance();
+        $generator->create_instance();
+        $bi = $generator->create_instance();
+        $this->assertEquals($beforeblocks+3, $DB->count_records('block_instances'));
+
+    }
+}
index b8385da..9eb1e8f 100644 (file)
@@ -292,9 +292,16 @@ class blog_entry {
 
         $contentcell->text .= $OUTPUT->container_start('commands');
 
-        if (blog_user_can_edit_entry($this) && empty($this->uniquehash)) {
-            $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php', array('action' => 'edit', 'entryid' => $this->id)), $stredit) . ' | ';
-            $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php', array('action' => 'delete', 'entryid' => $this->id)), $strdelete) . ' | ';
+        if (blog_user_can_edit_entry($this)) {
+            if (empty($this->uniquehash)) {
+                //External blog entries should not be edited
+                $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php',
+                                                        array('action' => 'edit', 'entryid' => $this->id)),
+                                                        $stredit) . ' | ';
+            }
+            $contentcell->text .= html_writer::link(new moodle_url('/blog/edit.php',
+                                                    array('action' => 'delete', 'entryid' => $this->id)),
+                                                    $strdelete) . ' | ';
         }
 
         $contentcell->text .= html_writer::link(new moodle_url('/blog/index.php', array('entryid' => $this->id)), get_string('permalink', 'blog'));
index 3ad949c..f385eb3 100644 (file)
@@ -14,7 +14,6 @@
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-
 /**
  * Unit tests for blog
  *
@@ -35,19 +34,23 @@ require_once($CFG->dirroot . '/blog/lib.php');
 class bloglib_testcase extends advanced_testcase {
 
     private $courseid; // To store important ids to be used in tests
+    private $cmid;
     private $groupid;
     private $userid;
     private $tagid;
+    private $postid;
 
     protected function setUp() {
         global $DB;
         parent::setUp();
 
-        $this->resetAfterTest(true);
+        $this->resetAfterTest();
 
         // Create default course
-        $course = $this->getDataGenerator()->create_course(array('category'=>1, 'fullname'=>'Anonymous test course', 'shortname'=>'ANON'));
+        $course = $this->getDataGenerator()->create_course(array('category'=>1, 'shortname'=>'ANON'));
+        $this->assertNotEmpty($course);
         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
+        $this->assertNotEmpty($page);
 
         // Create default group
         $group = new stdClass();
@@ -75,17 +78,20 @@ class bloglib_testcase extends advanced_testcase {
 
         // Grab important ids
         $this->courseid = $course->id;
+        $this->cmid = $page->cmid;
         $this->groupid  = $group->id;
         $this->userid  = $user->id;
         $this->tagid  = $tag->id;
+        $this->postid = $post->id;
     }
 
 
     public function test_overrides() {
+        global $SITE;
 
         // Try all the filters at once: Only the entry filter is active
-        $filters = array('site' => 1, 'course' => $this->courseid, 'module' => 1,
-            'group' => $this->groupid, 'user' => 1, 'tag' => 1, 'entry' => 1);
+        $filters = array('site' => $SITE->id, 'course' => $this->courseid, 'module' => $this->cmid,
+            'group' => $this->groupid, 'user' => $this->userid, 'tag' => $this->tagid, 'entry' => $this->postid);
         $blog_listing = new blog_listing($filters);
         $this->assertFalse(array_key_exists('site', $blog_listing->filters));
         $this->assertFalse(array_key_exists('course', $blog_listing->filters));
@@ -96,8 +102,8 @@ class bloglib_testcase extends advanced_testcase {
         $this->assertTrue(array_key_exists('entry', $blog_listing->filters));
 
         // Again, but without the entry filter: This time, the tag, user and module filters are active
-        $filters = array('site' => 1, 'course' => $this->courseid, 'module' => 1,
-            'group' => $this->groupid, 'user' => 1, 'tag' => 1);
+        $filters = array('site' => $SITE->id, 'course' => $this->courseid, 'module' => $this->cmid,
+            'group' => $this->groupid, 'user' => $this->userid, 'tag' => $this->postid);
         $blog_listing = new blog_listing($filters);
         $this->assertFalse(array_key_exists('site', $blog_listing->filters));
         $this->assertFalse(array_key_exists('course', $blog_listing->filters));
@@ -107,7 +113,7 @@ class bloglib_testcase extends advanced_testcase {
         $this->assertTrue(array_key_exists('tag', $blog_listing->filters));
 
         // We should get the same result by removing the 3 inactive filters: site, course and group:
-        $filters = array('module' => 1, 'user' => 1, 'tag' => 1);
+        $filters = array('module' => $this->cmid, 'user' => $this->userid, 'tag' => $this->tagid);
         $blog_listing = new blog_listing($filters);
         $this->assertFalse(array_key_exists('site', $blog_listing->filters));
         $this->assertFalse(array_key_exists('course', $blog_listing->filters));
@@ -135,9 +141,10 @@ class bloglib_testcase extends advanced_testcase {
 
     public function test_blog_get_headers_case_7() {
         global $CFG, $PAGE, $OUTPUT;
-        $blog_headers = blog_get_headers(NULL, 1);
+        $blog_headers = blog_get_headers(NULL, $this->groupid);
         $this->assertNotEquals($blog_headers['heading'], '');
     }
+
     public function test_blog_get_headers_case_10() {
         global $CFG, $PAGE, $OUTPUT;
         $blog_headers = blog_get_headers($this->courseid);
index 36eb3d9..0aec77a 100644 (file)
@@ -490,12 +490,6 @@ $CFG->admin = 'admin';
 // $CFG->phpunit_prefix = 'phpu_';
 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
 // $CFG->phpunit_directorypermissions = 02777; // optional
-// $CFG->phpunit_extra_drivers = array(
-//      1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'),
-//      2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'),
-//      3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'),
-//      4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'),
-// ); // for database driver testing only, DB is selected via PHPUNIT_TEST_DRIVER=n
 
 //=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
index 7601fea..402bc84 100644 (file)
@@ -783,7 +783,7 @@ class grade_report_grader extends grade_report {
                     $headerlink = $this->gtree->get_element_header($element, true, $this->get_pref('showactivityicons'), false);
 
                     $itemcell = new html_table_cell();
-                    $itemcell->attributes['class'] = $type . ' ' . $catlevel . 'highlightable';
+                    $itemcell->attributes['class'] = $type . ' ' . $catlevel . ' highlightable';
 
                     if ($element['object']->is_hidden()) {
                         $itemcell->attributes['class'] .= ' hidden';
index 4885770..e18d2c9 100644 (file)
--- a/index.php
+++ b/index.php
@@ -35,7 +35,7 @@
     redirect_if_major_upgrade_required();
 
     $urlparams = array();
-    if ($CFG->defaulthomepage == HOMEPAGE_MY && optional_param('redirect', 1, PARAM_BOOL) === 0) {
+    if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY) && optional_param('redirect', 1, PARAM_BOOL) === 0) {
         $urlparams['redirect'] = 0;
     }
     $PAGE->set_url('/', $urlparams);
@@ -62,9 +62,9 @@
         // Redirect logged-in users to My Moodle overview if required
         if (optional_param('setdefaulthome', false, PARAM_BOOL)) {
             set_user_preference('user_home_page_preference', HOMEPAGE_SITE);
-        } else if ($CFG->defaulthomepage == HOMEPAGE_MY && optional_param('redirect', 1, PARAM_BOOL) === 1) {
+        } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY) && optional_param('redirect', 1, PARAM_BOOL) === 1) {
             redirect($CFG->wwwroot .'/my/');
-        } else if (!empty($CFG->defaulthomepage) && $CFG->defaulthomepage == HOMEPAGE_USER) {
+        } else if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER)) {
             $PAGE->settingsnav->get('usercurrentsettings')->add(get_string('makethismyhome'), new moodle_url('/', array('setdefaulthome'=>true)), navigation_node::TYPE_SETTING);
         }
     }
index ddd0051..f62cb9c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatelangdir'] = 'Verzeichnis "lang" kann nicht angelegt werden';
-$string['cannotcreatetempdir'] = 'Das Verzeichnis "temp" kann nicht angelegt werden.';
-$string['cannotdownloadcomponents'] = 'Einige Komponenten können nicht heruntergeladen werden.';
+$string['cannotcreatelangdir'] = 'Verzeichnis \'lang\' wurde nicht angelegt';
+$string['cannotcreatetempdir'] = 'Das Verzeichnis \'temp\' wurde nicht angelegt';
+$string['cannotdownloadcomponents'] = 'Einige Komponenten können nicht geladen werden.';
 $string['cannotdownloadzipfile'] = 'ZIP-Datei kann nicht heruntergeladen werden.';
-$string['cannotfindcomponent'] = 'Eine Komponente kann nicht gefunden werden';
-$string['cannotsavemd5file'] = 'Die md5-Datei kann nicht gespeichert werden';
-$string['cannotsavezipfile'] = 'Die ZIP-Datei kann nicht gespeichert werden';
+$string['cannotfindcomponent'] = 'Komponente wurde nicht gefunden';
+$string['cannotsavemd5file'] = 'Die md5-Datei wurde nicht gespeichert';
+$string['cannotsavezipfile'] = 'Die ZIP-Datei wurde nicht gespeichert';
 $string['cannotunzipfile'] = 'Die Datei kann nicht entpackt werden';
 $string['componentisuptodate'] = 'Die Komponente ist aktuell.';
 $string['downloadedfilecheckfailed'] = 'Die Überprüfung der heruntergeladenen Datei ist gescheitert';
diff --git a/install/lang/de_comm/langconfig.php b/install/lang/de_comm/langconfig.php
new file mode 100644 (file)
index 0000000..636a641
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle 2.3dev installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'de';
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'Deutsch community';
index 0af62aa..e8c2a70 100644 (file)
@@ -203,6 +203,7 @@ $string['rootsettingactivities'] = 'Include activities';
 $string['rootsettingblocks'] = 'Include blocks';
 $string['rootsettingfilters'] = 'Include filters';
 $string['rootsettingcomments'] = 'Include comments';
+$string['rootsettingcalendarevents'] = 'Include calendar events';
 $string['rootsettinguserscompletion'] = 'Include user completion details';
 $string['rootsettinglogs'] = 'Include course logs';
 $string['rootsettinggradehistories'] = 'Include grade history';
index 8fdab4f..74edbbc 100644 (file)
@@ -220,6 +220,7 @@ $string['nocate'] = 'No such category {$a}!';
 $string['nopermissionadd'] = 'You don\'t have permission to add questions here.';
 $string['nopermissionmove'] = 'You don\'t have permission to move questions from here. You must save the question in this category or save it as a new question.';
 $string['noprobs'] = 'No problems found in your question database.';
+$string['noquestions'] = 'No questions were found that could be exported. Make sure that you have selected a category to export that contains questions.';
 $string['noquestionsinfile'] = 'There are no questions in the import file';
 $string['notenoughanswers'] = 'This type of question requires at least {$a} answers';
 $string['notenoughdatatoeditaquestion'] = 'Neither a question id, nor a category id and question type, was specified.';
@@ -323,14 +324,18 @@ $string['fillincorrect'] = 'Fill in correct responses';
 $string['flagged'] = 'Flagged';
 $string['flagthisquestion'] = 'Flag this question';
 $string['generalfeedback'] = 'General feedback';
-$string['generalfeedback_help'] = 'General feedback is shown to the student after they have attempted the question. Unlike feedback, which depends on the question type and what response the student gave, the same general feedback text is shown to all students.
+$string['generalfeedback_help'] = 'General feedback is shown to the student after they have completed the question. Unlike specific feedback, which depends on the question type and what response the student gave, the same general feedback text is shown to all students.
 
-You can use the general feedback to give students some background to what knowledge the question was testing, or give them a link to more information they can use if they did not understand the questions.';
+You can use the general feedback to give students a fully worked answer and perhaps a link to more information they can use if they did not understand the questions.';
 $string['hidden'] = 'Hidden';
 $string['hintn'] = 'Hint {no}';
 $string['hinttext'] = 'Hint text';
 $string['howquestionsbehave'] = 'How questions behave';
-$string['howquestionsbehave_help'] = 'Students can interact with the questions in the quiz in various different ways. For example, you may wish the students to enter an answer to each question and then submit the entire quiz, before anything is graded or they get any feedback. That would be \'Deferred feedback\' mode. Alternatively, you may wish for students to submit each question as they go along to get immediate feedback, and if they do not get it right immediately, have another try for fewer marks. That would be \'Interactive with multiple tries\' mode.';
+$string['howquestionsbehave_help'] = 'Students can interact with the questions in the quiz in various different ways. For example, you may wish the students to enter an answer to each question and then submit the entire quiz, before anything is graded or they get any feedback. That would be \'Deferred feedback\' mode.
+
+Alternatively, you may wish for students to submit each question as they go along to get immediate feedback, and if they do not get it right immediately, have another try for fewer marks. That would be \'Interactive with multiple tries\' mode.
+
+Those are probably the two most commonly used modes of behaviour. ';
 $string['importfromcoursefiles'] = '... or choose a course file to import.';
 $string['importfromupload'] = 'Select a file to upload ...';
 $string['includesubcategories'] = 'Also show questions from sub-categories';
@@ -377,6 +382,7 @@ $string['responsehistory'] = 'Response history';
 $string['restart'] = 'Start again';
 $string['restartwiththeseoptions'] = 'Start again with these options';
 $string['rightanswer'] = 'Right answer';
+$string['rightanswer_help'] = 'an automatically generated summary of the correct response. This can be limited, so you may wish to consider explaining the correct solution in the general feedback for the question, and turning this option off.';
 $string['saved'] = 'Saved: {$a}';
 $string['saveflags'] = 'Save the state of the flags';
 $string['settingsformultipletries'] = 'Settings for multiple tries';
@@ -388,6 +394,7 @@ $string['shown'] = 'Shown';
 $string['shownumpartscorrect'] = 'Show the number of correct responses';
 $string['shownumpartscorrectwhenfinished'] = 'Show the number of correct responses once the question has finished';
 $string['specificfeedback'] = 'Specific feedback';
+$string['specificfeedback_help'] = 'Feedback that depends on what response the student gave.';
 $string['started'] = 'Started';
 $string['state'] = 'State';
 $string['step'] = 'Step';
@@ -401,6 +408,7 @@ $string['unknownquestion'] = 'Unknown question: {$a}.';
 $string['unknownquestioncatregory'] = 'Unknown question category: {$a}.';
 $string['unknownquestiontype'] = 'Unknown question type: {$a}.';
 $string['whethercorrect'] = 'Whether correct';
+$string['whethercorrect_help'] = 'This covers both the textual description \'Correct\', \'Partially correct\' or \'Incorrect\', and any coloured highlighting that conveys the same information.';
 $string['withselected'] = 'With selected';
 $string['xoutofmax'] = '{$a->mark} out of {$a->max}';
 $string['yougotnright'] = 'You have correctly selected {$a->num}.';
index 48c9099..d26e0f9 100644 (file)
@@ -269,6 +269,10 @@ function uninstall_plugin($type, $name) {
 
     // perform clean-up task common for all the plugin/subplugin types
 
+    //delete the web service functions and pre-built services
+    require_once($CFG->dirroot.'/lib/externallib.php');
+    external_delete_descriptions($component);
+
     // delete calendar events
     $DB->delete_records('event', array('modulename' => $pluginname));
 
index b39de3f..32272de 100644 (file)
@@ -24,7 +24,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-defined('MOODLE_INTERNAL') || die();
+// NOTE: no MOODLE_INTERNAL test here, sometimes we use this before requiring Moodle libs!
 
 /**
  * Get input from user
index 74fc4ef..e0e6c91 100644 (file)
@@ -650,7 +650,8 @@ function notify_login_failures() {
 
     // Now, select all the login error logged records belonging to the ips and infos
     // since lastnotifyfailure, that we have stored in the cache_flags table
-    $sql = "SELECT l.*, u.firstname, u.lastname
+    $sql = "SELECT * FROM (
+        SELECT l.*, u.firstname, u.lastname
               FROM {log} l
               JOIN {cache_flags} cf ON l.ip = cf.name
          LEFT JOIN {user} u         ON l.userid = u.id
@@ -664,8 +665,8 @@ function notify_login_failures() {
          LEFT JOIN {user} u         ON l.userid = u.id
              WHERE l.module = 'login' AND l.action = 'error'
                    AND l.time > ?
-                   AND cf.flagtype = 'login_failure_by_info'
-          ORDER BY time DESC";
+                   AND cf.flagtype = 'login_failure_by_info') t
+        ORDER BY t.time DESC";
     $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure);
 
     // Init some variables
index b24ab45..aa684b6 100644 (file)
@@ -82,12 +82,22 @@ class mssql_sql_generator extends sql_generator {
             $table = new xmldb_table($table);
         }
 
-        // From http://msdn.microsoft.com/en-us/library/ms176057.aspx
         $value = (int)$this->mdb->get_field_sql('SELECT MAX(id) FROM {'. $table->getName() . '}');
+        $sqls = array();
+
+        // MSSQL has one non-consistent behavior to create the first identity value, depending
+        // if the table has been truncated or no. If you are really interested, you can find the
+        // whole description of the problem at:
+        //     http://www.justinneff.com/archive/tag/dbcc-checkident
         if ($value == 0) {
+            // truncate to get consistent result from reseed
+            $sqls[] = "TRUNCATE TABLE " . $this->getTableName($table);
             $value = 1;
         }
-        return array("DBCC CHECKIDENT ('" . $this->getTableName($table) . "', RESEED, $value)");
+
+        // From http://msdn.microsoft.com/en-us/library/ms176057.aspx
+        $sqls[] = "DBCC CHECKIDENT ('" . $this->getTableName($table) . "', RESEED, $value)";
+        return $sqls;
     }
 
     /**
index 56e4583..3224ecd 100644 (file)
@@ -1626,7 +1626,14 @@ class ddl_test extends UnitTestCase {
 
         $record = (object)array('id'=>666, 'course'=>10);
         $DB->import_record('testtable', $record);
-        $DB->delete_records('testtable');
+        $DB->delete_records('testtable'); // This delete performs one TRUNCATE
+
+        $dbman->reset_sequence($table); // using xmldb object
+        $this->assertEqual(1, $DB->insert_record('testtable', (object)array('course'=>13)));
+
+        $record = (object)array('id'=>666, 'course'=>10);
+        $DB->import_record('testtable', $record);
+        $DB->delete_records('testtable', array()); // This delete performs one DELETE
 
         $dbman->reset_sequence($table); // using xmldb object
         $this->assertEqual(1, $DB->insert_record('testtable', (object)array('course'=>13)));
index 9f7385a..41bd729 100644 (file)
@@ -31,10 +31,8 @@ class ddl_testcase extends database_driver_testcase {
     private $records= array();
 
     protected function setUp() {
-        //global $CFG;
-        //require_once($CFG->libdir . '/adminlib.php');
-
         parent::setUp();
+        $dbman = $this->tdb->get_manager(); // loads DDL libs
 
         $table = new xmldb_table('test_table0');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
@@ -131,6 +129,8 @@ class ddl_testcase extends database_driver_testcase {
      * Fill the given test table with some records, as far as
      * DDL behaviour must be tested both with real data and
      * with empty tables
+     * @param string $tablename
+     * @return int count of records
      */
     private function fill_deftable($tablename) {
         $DB = $this->tdb; // do not use global $DB!
@@ -1600,7 +1600,14 @@ class ddl_testcase extends database_driver_testcase {
 
         $record = (object)array('id'=>666, 'course'=>10);
         $DB->import_record('testtable', $record);
-        $DB->delete_records('testtable');
+        $DB->delete_records('testtable'); // This delete performs one TRUNCATE
+
+        $dbman->reset_sequence($table); // using xmldb object
+        $this->assertEquals(1, $DB->insert_record('testtable', (object)array('course'=>13)));
+
+        $record = (object)array('id'=>666, 'course'=>10);
+        $DB->import_record('testtable', $record);
+        $DB->delete_records('testtable', array()); // This delete performs one DELETE
 
         $dbman->reset_sequence($table); // using xmldb object
         $this->assertEquals(1, $DB->insert_record('testtable', (object)array('course'=>13)));
index a31d362..b619883 100644 (file)
@@ -152,7 +152,6 @@ class database_column_info {
 
         switch ($this->meta_type) {
             case 'R': // normalise counters (usually 'id')
-                $this->auto_increment = true;
                 $this->binary         = false;
                 $this->has_default    = false;
                 $this->default_value  = null;
index 119acc8..faf097e 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -324,7 +323,11 @@ abstract class moodle_database {
             $lowesttransaction = end($this->transactions);
             $backtrace = $lowesttransaction->get_backtrace();
 
-            error_log('Potential coding error - active database transaction detected when disposing database:'."\n".format_backtrace($backtrace, true));
+            if (defined('PHPUNIT_TEST') and PHPUNIT_TEST) {
+                //no need to log sudden exits in our PHPunit test cases
+            } else {
+                error_log('Potential coding error - active database transaction detected when disposing database:'."\n".format_backtrace($backtrace, true));
+            }
             $this->force_transaction_rollback();
         }
         if ($this->used_for_db_sessions) {
@@ -419,7 +422,7 @@ abstract class moodle_database {
 
     /**
      * This logs the last query based on 'logall', 'logslow' and 'logerrors' options configured via $CFG->dboptions .
-     * @param mixed string error or false if not error
+     * @param string $error or false if not error
      * @return void
      */
     public function query_log($error=false) {
@@ -1036,7 +1039,7 @@ abstract class moodle_database {
     public function get_recordset_list($table, $field, array $values, $sort='', $fields='*', $limitfrom=0, $limitnum=0) {
         list($select, $params) = $this->where_clause_list($field, $values);
         if (empty($select)) {
-            $select = '1 = 2'; /// Fake condition, won't return rows ever. MDL-17645
+            $select = '1 = 2'; // Fake condition, won't return rows ever. MDL-17645
             $params = array();
         }
         return $this->get_recordset_select($table, $select, $params, $sort, $fields, $limitfrom, $limitnum);
@@ -1126,6 +1129,8 @@ abstract class moodle_database {
      * @param string $fields A comma separated list of fields to be returned from the chosen table. If specified,
      *   the first field should be a unique one such as 'id' since it will be used as a key in the associative
      *   array.
+     * @param int $limitfrom return a subset of records, starting at this point (optional).
+     * @param int $limitnum return a subset comprising this many records in total (optional).
      * @return array An array of objects indexed by first column
      * @throws dml_exception A DML specific exception is thrown for any errors.
      */
@@ -1293,10 +1298,11 @@ abstract class moodle_database {
      * @param string $table The database table to be checked against.
      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
      * @param array $params array of sql parameters
+     * @param string $fields A comma separated list of fields to be returned from the chosen table.
      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
      *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
      *                        MUST_EXIST means throw exception if no record or multiple records found
-     * @return mixed a fieldset object containing the first matching record, false or exception if error not found depending on mode
+     * @return stdClass|false a fieldset object containing the first matching record, false or exception if error not found depending on mode
      * @throws dml_exception A DML specific exception is thrown for any errors.
      */
     public function get_record_select($table, $select, array $params=null, $fields='*', $strictness=IGNORE_MISSING) {
@@ -1396,8 +1402,6 @@ abstract class moodle_database {
     /**
      * Get a single field value (first field) using a SQL statement.
      *
-     * @param string $table the table to query.
-     * @param string $return the field to return the value of.
      * @param string $sql The SQL query returning one row with one column
      * @param array $params array of sql parameters
      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
@@ -1461,8 +1465,9 @@ abstract class moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
+     * @param object $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
+     * @param bool $bulk Set to true is multiple inserts are expected
      * @return bool|int true or new id
      * @throws dml_exception A DML specific exception is thrown for any errors.
      */
@@ -1682,7 +1687,7 @@ abstract class moodle_database {
 
 
 
-/// sql constructs
+// sql constructs
     /**
      * Returns the FROM clause required by some DBs in all SELECT statements.
      *
@@ -2045,7 +2050,7 @@ abstract class moodle_database {
         return '';
     }
 
-/// transactions
+// transactions
 
     /**
      * Checks and returns true if transactions are supported.
@@ -2118,6 +2123,7 @@ abstract class moodle_database {
      * Indicates delegated transaction finished successfully.
      * The real database transaction is committed only if
      * all delegated transactions committed.
+     * @param moodle_transaction $transaction The transaction to commit
      * @return void
      * @throws dml_transaction_exception Creates and throws transaction related exceptions.
      */
@@ -2226,7 +2232,7 @@ abstract class moodle_database {
         $this->force_rollback = false;
     }
 
-/// session locking
+// session locking
     /**
      * Is session lock supported in this driver?
      * @return bool
@@ -2255,7 +2261,7 @@ abstract class moodle_database {
     public function release_session_lock($rowid) {
     }
 
-/// performance and logging
+// performance and logging
     /**
      * Returns the number of reads done by this database.
      * @return int Number of reads.
index 6e8b357..212f44f 100644 (file)
@@ -287,6 +287,13 @@ class mssql_native_moodle_database extends moodle_database {
         return $info;
     }
 
+    /**
+     * Returns if the RDBMS server fulfills the required version
+     *
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Delete this unused and protected method. MDL-32392
+     */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
         $server = $server['version'];
@@ -805,6 +812,7 @@ class mssql_native_moodle_database extends moodle_database {
         }
 
         $returning = "";
+        $isidentity = false;
 
         if ($customsequence) {
             if (!isset($params['id'])) {
@@ -812,13 +820,21 @@ class mssql_native_moodle_database extends moodle_database {
             }
             $returnid = false;
 
-            // Disable IDENTITY column before inserting record with id
-            $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
-            list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $this->free_result($result);
+            $columns = $this->get_columns($table);
+            if (isset($columns['id']) and $columns['id']->auto_increment) {
+                $isidentity = true;
+            }
+
+            // Disable IDENTITY column before inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
+                list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
+                $this->query_start($sql, null, SQL_QUERY_AUX);
+                $result = mssql_query($sql, $this->mssql);
+                $this->query_end($result);
+                $this->free_result($result);
+            }
 
         } else {
             unset($params['id']);
@@ -851,13 +867,16 @@ class mssql_native_moodle_database extends moodle_database {
         $this->free_result($result);
 
         if ($customsequence) {
-            // Enable IDENTITY column after inserting record with id
-            $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
-            list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $this->free_result($result);
+            // Enable IDENTITY column after inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
+                list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
+                $this->query_start($sql, null, SQL_QUERY_AUX);
+                $result = mssql_query($sql, $this->mssql);
+                $this->query_end($result);
+                $this->free_result($result);
+            }
         }
 
         if (!$returnid) {
index 1920973..c834eb4 100644 (file)
@@ -485,7 +485,6 @@ class mysqli_native_moodle_database extends moodle_database {
                 $rawcolumn->is_nullable              = $rawcolumn->null; unset($rawcolumn->null);
                 $rawcolumn->column_default           = $rawcolumn->default; unset($rawcolumn->default);
                 $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->default);
-                $rawcolumn->extra                    = ($rawcolumn->column_name === 'id') ? 'auto_increment' : '';
 
                 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
                     $rawcolumn->data_type = $matches[1];
@@ -955,7 +954,7 @@ class mysqli_native_moodle_database extends moodle_database {
         $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
         $this->query_end($result);
 
-        if (!$id) {
+        if (!$customsequence and !$id) {
             throw new dml_write_exception('unknown error fetching inserted id');
         }
 
index a529277..046e377 100644 (file)
@@ -296,6 +296,13 @@ class oci_native_moodle_database extends moodle_database {
         return $info;
     }
 
+    /**
+     * Returns if the RDBMS server fulfills the required version
+     *
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Delete this unused and protected method. MDL-32392
+     */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
         $server = $server['version'];
@@ -493,9 +500,13 @@ class oci_native_moodle_database extends moodle_database {
 
         // We give precedence to CHAR_LENGTH for VARCHAR2 columns over WIDTH because the former is always
         // BYTE based and, for cross-db operations, we want CHAR based results. See MDL-29415
-        $sql = "SELECT CNAME, COLTYPE, nvl(CHAR_LENGTH, WIDTH) AS WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL
+        // Instead of guessing sequence based exclusively on name, check tables against user_triggers to
+        // ensure the table has a 'before each row' trigger to assume 'id' is auto_increment. MDL-32365
+        $sql = "SELECT CNAME, COLTYPE, nvl(CHAR_LENGTH, WIDTH) AS WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL,
+                  DECODE(NVL(TRIGGER_NAME, '0'), '0', '0', '1') HASTRIGGER
                   FROM COL c
              LEFT JOIN USER_TAB_COLUMNS u ON (u.TABLE_NAME = c.TNAME AND u.COLUMN_NAME = c.CNAME AND u.DATA_TYPE = 'VARCHAR2')
+             LEFT JOIN USER_TRIGGERS t ON (t.TABLE_NAME = c.TNAME AND TRIGGER_TYPE = 'BEFORE EACH ROW' AND c.CNAME = 'ID')
                  WHERE TNAME = UPPER('{" . $table . "}')
               ORDER BY COLNO";
 
@@ -517,6 +528,7 @@ class oci_native_moodle_database extends moodle_database {
 
             $info = new stdClass();
             $info->name = strtolower($rawcolumn->CNAME);
+            $info->auto_increment = ((int)$rawcolumn->HASTRIGGER) ? true : false;
             $matches = null;
 
             if ($rawcolumn->COLTYPE === 'VARCHAR2'
@@ -550,7 +562,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->binary        = false;
                 $info->unsigned      = null;
-                $info->auto_increment= false;
                 $info->unique        = null;
 
             } else if ($rawcolumn->COLTYPE === 'NUMBER') {
@@ -563,13 +574,11 @@ class oci_native_moodle_database extends moodle_database {
                         $info->primary_key   = true;
                         $info->meta_type     = 'R';
                         $info->unique        = true;
-                        $info->auto_increment= true;
                         $info->has_default   = false;
                     } else {
                         $info->primary_key   = false;
                         $info->meta_type     = 'I';
                         $info->unique        = null;
-                        $info->auto_increment= false;
                     }
                     $info->scale = null;
 
@@ -578,7 +587,6 @@ class oci_native_moodle_database extends moodle_database {
                     $info->meta_type     = 'N';
                     $info->primary_key   = false;
                     $info->unsigned      = null;
-                    $info->auto_increment= false;
                     $info->unique        = null;
                     $info->scale         = $rawcolumn->SCALE;
                 }
@@ -596,7 +604,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->meta_type     = 'N';
                 $info->unique        = null;
-                $info->auto_increment= false;
                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
                 if ($info->has_default) {
@@ -632,7 +639,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->binary        = false;
                 $info->unsigned      = null;
-                $info->auto_increment= false;
                 $info->unique        = null;
 
             } else if ($rawcolumn->COLTYPE === 'BLOB') {
@@ -661,7 +667,6 @@ class oci_native_moodle_database extends moodle_database {
                 $info->primary_key   = false;
                 $info->binary        = true;
                 $info->unsigned      = null;
-                $info->auto_increment= false;
                 $info->unique        = null;
 
             } else {
index 3d9771d..f8367fe 100644 (file)
@@ -179,8 +179,14 @@ class pgsql_native_moodle_database extends moodle_database {
         pg_set_client_encoding($this->pgsql, 'utf8');
         $this->query_end(true);
 
-        // find out the bytea oid
-        $sql = "SELECT oid FROM pg_type WHERE typname = 'bytea'";
+        $sql = '';
+        // Only for 9.0 and upwards, set bytea encoding to old format.
+        if ($this->is_min_version('9.0')) {
+            $sql = "SET bytea_output = 'escape'; ";
+        }
+
+        // Find out the bytea oid.
+        $sql .= "SELECT oid FROM pg_type WHERE typname = 'bytea'";
         $this->query_start($sql, null, SQL_QUERY_AUX);
         $result = pg_query($this->pgsql, $sql);
         $this->query_end($result);
@@ -251,6 +257,13 @@ class pgsql_native_moodle_database extends moodle_database {
         return array('description'=>$info['server'], 'version'=>$info['server']);
     }
 
+    /**
+     * Returns if the RDBMS server fulfills the required version
+     *
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Make this method private. MDL-32392
+     */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
         $server = $server['version'];
index 6a9c832..d3261c9 100644 (file)
@@ -825,6 +825,18 @@ class dml_test extends UnitTestCase {
         // Test get_columns for non-existing table returns empty array. MDL-30147
         $columns = $DB->get_columns('xxxx');
         $this->assertEqual(array(), $columns);
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $columns = $DB->get_columns($tablename);
+        $this->assertFalse($columns['id']->auto_increment);
     }
 
     public function test_get_manager() {
@@ -1848,6 +1860,20 @@ class dml_test extends UnitTestCase {
         } catch (dml_exception $ex) {
             $this->assertTrue(true);
         }
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $record = (object)array('id'=>5, 'course' => 1);
+        $DB->insert_record_raw($tablename, $record, false, false, true);
+        $record = $DB->get_record($tablename, array());
+        $this->assertEqual(5, $record->id);
     }
 
     public function test_insert_record() {
index 5fe2ab2..996b4e8 100644 (file)
@@ -152,7 +152,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
          * Log all Errors.
          */
         sqlsrv_configure("WarningsReturnAsErrors", FALSE);
-        sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_ALL);
+        sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF);
         sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR);
 
         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
@@ -277,10 +277,11 @@ class sqlsrv_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Get the minimum SQL allowed
+     * Returns if the RDBMS server fulfills the required version
      *
-     * @param mixed $version
-     * @return mixed
+     * @param string $version version to check against
+     * @return bool returns if the version is fulfilled (true) or no (false)
+     * @todo Delete this unused and protected method. MDL-32392
      */
     protected function is_min_version($version) {
         $server = $this->get_server_info();
@@ -391,7 +392,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         $this->tables = array ();
         $prefix = str_replace('_', '\\_', $this->prefix);
         $sql = "SELECT table_name
-                  FROM information_schema.tables
+                  FROM INFORMATION_SCHEMA.TABLES
                  WHERE table_name LIKE '$prefix%' ESCAPE '\\' AND table_type = 'BASE TABLE'";
 
         $this->query_start($sql, null, SQL_QUERY_AUX);
@@ -497,7 +498,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
                            is_nullable AS is_nullable,
                            columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
                            column_default AS default_value
-                      FROM information_schema.columns
+                      FROM INFORMATION_SCHEMA.COLUMNS
                      WHERE table_name = '{".$table."}'
                   ORDER BY ordinal_position";
         } else { // temp table, get metadata from tempdb schema
@@ -509,7 +510,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
                            is_nullable AS is_nullable,
                            columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
                            column_default AS default_value
-                      FROM tempdb.information_schema.columns ".
+                      FROM tempdb.INFORMATION_SCHEMA.COLUMNS ".
             // check this statement
             // JOIN tempdb..sysobjects ON name = table_name
             // WHERE id = object_id('tempdb..{".$table."}')
@@ -871,14 +872,26 @@ class sqlsrv_native_moodle_database extends moodle_database {
         if (!is_array($params)) {
             $params = (array)$params;
         }
+
+        $isidentity = false;
+
         if ($customsequence) {
             if (!isset($params['id'])) {
                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
             }
+
             $returnid = false;
-            // Disable IDENTITY column before inserting record with id
-            $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!!
-            $this->do_query($sql, null, SQL_QUERY_AUX);
+            $columns = $this->get_columns($table);
+            if (isset($columns['id']) and $columns['id']->auto_increment) {
+                $isidentity = true;
+            }
+
+            // Disable IDENTITY column before inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!!
+                $this->do_query($sql, null, SQL_QUERY_AUX);
+            }
 
         } else {
             unset($params['id']);
@@ -894,9 +907,12 @@ class sqlsrv_native_moodle_database extends moodle_database {
         $query_id = $this->do_query($sql, $params, SQL_QUERY_INSERT);
 
         if ($customsequence) {
-            // Enable IDENTITY column after inserting record with id
-            $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!!
-            $this->do_query($sql, null, SQL_QUERY_AUX);
+            // Enable IDENTITY column after inserting record with id, only if the
+            // column is identity, from meta information.
+            if ($isidentity) {
+                $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!!
+                $this->do_query($sql, null, SQL_QUERY_AUX);
+            }
         }
 
         if ($returnid) {
index 9fe16a4..e91dbdf 100644 (file)
@@ -28,6 +28,11 @@ defined('MOODLE_INTERNAL') || die();
 
 class dml_testcase extends database_driver_testcase {
 
+    protected function setUp() {
+        parent::setUp();
+        $dbman = $this->tdb->get_manager(); // loads DDL libs
+    }
+
     /**
      * Get a xmldb_table object for testing, deleting any existing table
      * of the same name, for example if one was left over from a previous test
@@ -778,6 +783,18 @@ class dml_testcase extends database_driver_testcase {
         // Test get_columns for non-existing table returns empty array. MDL-30147
         $columns = $DB->get_columns('xxxx');
         $this->assertEquals(array(), $columns);
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $columns = $DB->get_columns($tablename);
+        $this->assertFalse($columns['id']->auto_increment);
     }
 
     public function test_get_manager() {
@@ -1801,6 +1818,20 @@ class dml_testcase extends database_driver_testcase {
         } catch (dml_exception $ex) {
             $this->assertTrue(true);
         }
+
+        // create something similar to "context_temp" with id column without sequence
+        $dbman->drop_table($table);
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $record = (object)array('id'=>5, 'course' => 1);
+        $DB->insert_record_raw($tablename, $record, false, false, true);
+        $record = $DB->get_record($tablename, array());
+        $this->assertEquals(5, $record->id);
     }
 
     public function test_insert_record() {
@@ -4372,7 +4403,7 @@ class dml_testcase extends database_driver_testcase {
 
         // make sure reserved words do not cause fatal problems in query parameters
 
-        $DB->execute("UPDATE {{$tablename}} SET course = 1 WHERE ID = :select", array('select'=>1));
+        $DB->execute("UPDATE {{$tablename}} SET course = 1 WHERE id = :select", array('select'=>1));
         $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
         $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
         $rs->close();
index befe5d2..c90139d 100644 (file)
@@ -204,11 +204,8 @@ function enrol_check_plugins($user) {
         return;
     }
 
-    if (is_siteadmin()) {
-        // no sync for admin user, please use admin accounts only for admin tasks like the unix root user!
-        // if plugin fails on sync admins need to be able to log in and fix the settings
-        return;
-    }
+    // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
+    // which proved it was actually not necessary.
 
     static $inprogress = array();  // To prevent this function being called more than once in an invocation
 
@@ -264,8 +261,8 @@ function enrol_sharing_course($user1, $user2) {
 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
     global $DB, $CFG;
 
-    $user1 = !empty($user1->id) ? $user1->id : $user1;
-    $user2 = !empty($user2->id) ? $user2->id : $user2;
+    $user1 = isset($user1->id) ? $user1->id : $user1;
+    $user2 = isset($user2->id) ? $user2->id : $user2;
 
     if (empty($user1) or empty($user2)) {
         return false;
index 8875307..2cb1118 100644 (file)
@@ -487,4 +487,24 @@ function external_create_service_token($servicename, $context){
     global $USER, $DB;
     $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST);
     return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0);
+}
+
+/**
+ * Delete all pre-built services (+ related tokens) and external functions information defined in the specified component.
+ *
+ * @param string $component name of component (moodle, mod_assignment, etc.)
+ */
+function external_delete_descriptions($component) {
+    global $DB;
+
+    $params = array($component);
+
+    $DB->delete_records_select('external_tokens',
+            "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
+    $DB->delete_records_select('external_services_users',
+            "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
+    $DB->delete_records_select('external_services_functions',
+            "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", $params);
+    $DB->delete_records('external_services', array('component'=>$component));
+    $DB->delete_records('external_functions', array('component'=>$component));
 }
\ No newline at end of file
diff --git a/lib/grade/tests/fixtures/lib.php b/lib/grade/tests/fixtures/lib.php
new file mode 100644 (file)
index 0000000..7ca2520
--- /dev/null
@@ -0,0 +1,773 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/gradelib.php');
+
+
+/**
+ * Shared code for all grade related tests.
+ *
+ * Here is a brief explanation of the test data set up in these unit tests.
+ * category1 => array(category2 => array(grade_item1, grade_item2), category3 => array(grade_item3))
+ * 3 users for 3 grade_items
+ */
+class grade_base_testcase extends advanced_testcase {
+
+    protected $course;
+    protected $activities = array();
+    protected $grade_items = array();
+    protected $grade_categories = array();
+    protected $grade_grades = array();
+    protected $grade_outcomes = array();
+    protected $scale = array();
+    protected $scalemax = array();
+
+    protected $courseid;
+    protected $userid;
+
+    protected function setUp() {
+        global $CFG;
+        parent::setup();
+
+        $this->resetAfterTest(true);
+
+        $CFG->grade_droplow = -1;
+        $CFG->grade_keephigh = -1;
+        $CFG->grade_aggregation = -1;
+        $CFG->grade_aggregateonlygraded = -1;
+        $CFG->grade_aggregateoutcomes = -1;
+        $CFG->grade_aggregatesubcats = -1;
+
+        $this->course = $this->getDataGenerator()->create_course();
+        $this->courseid = $this->course->id;
+
+        $this->user[0] = $this->getDataGenerator()->create_user();
+        $this->user[1] = $this->getDataGenerator()->create_user();
+        $this->user[2] = $this->getDataGenerator()->create_user();
+        $this->user[3] = $this->getDataGenerator()->create_user();
+        $this->userid = $this->user[0]->id;
+
+        $this->load_modules();
+
+        $this->load_scales();
+        $this->load_grade_categories();
+        $this->load_grade_items();
+        $this->load_grade_grades();
+        $this->load_grade_outcomes();
+    }
+
+    public function test_void () {
+        // empty method to keep PHPUnit happy
+    }
+
+    private function load_modules() {
+        $this->activities[0] = $this->getDataGenerator()->create_module('assignment', array('course'=>$this->course->id));
+        $this->course_module[0] = get_coursemodule_from_instance('assignment', $this->activities[0]->id);
+
+        $this->activities[1] = $this->getDataGenerator()->create_module('assignment', array('course'=>$this->course->id));
+        $this->course_module[1] = get_coursemodule_from_instance('assignment', $this->activities[1]->id);
+
+        $this->activities[2] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[2] = get_coursemodule_from_instance('forum', $this->activities[2]->id);
+
+        $this->activities[3] = $this->getDataGenerator()->create_module('page', array('course'=>$this->course->id));
+        $this->course_module[3] = get_coursemodule_from_instance('page', $this->activities[3]->id);
+
+        $this->activities[4] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[4] = get_coursemodule_from_instance('forum', $this->activities[4]->id);
+
+        $this->activities[5] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[5] = get_coursemodule_from_instance('forum', $this->activities[5]->id);
+
+        $this->activities[6] = $this->getDataGenerator()->create_module('forum', array('course'=>$this->course->id));
+        $this->course_module[6] = get_coursemodule_from_instance('forum', $this->activities[6]->id);
+    }
+
+    private function load_scales() {
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale1';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Way off topic, Not very helpful, Fairly neutral, Fairly helpful, Supportive, Some good information, Perfect answer!';
+        $scale->description = 'This scale defines some of qualities that make posts helpful within the Moodle help forums.\n Your feedback will help others see how their posts are being received.';
+
+        $this->scale[0] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[0] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale2';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Distinction, Very Good, Good, Pass, Fail';
+        $scale->description = 'This scale is used to mark standard assignments.';
+
+        $this->scale[1] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[1] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale3';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Loner, Contentious, Disinterested, Participative, Follower, Leader';
+        $scale->description = 'Describes the level of teamwork of a student.';
+        $temp  = explode(',', $scale->scale);
+        $scale->max         = count($temp) -1;
+
+        $this->scale[2] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[2] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale4';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Does not understand theory, Understands theory but fails practice, Manages through, Excels';
+        $scale->description = 'Level of expertise at a technical task, with a theoretical framework.';
+        $temp  = explode(',', $scale->scale);
+        $scale->max         = count($temp) -1;
+
+        $this->scale[3] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[3] = substr_count($scale->scale, ',');
+
+        $scale = new stdClass();
+        $scale->name        = 'unittestscale5';
+        $scale->courseid    = $this->course->id;
+        $scale->userid      = $this->user[0]->id;
+        $scale->scale       = 'Insufficient, Acceptable, Excellent.';
+        $scale->description = 'Description of skills.';
+
+        $this->scale[4] = $this->getDataGenerator()->create_scale($scale);
+        $this->scalemax[4] = substr_count($scale->scale, ',');
+    }
+
+    /**
+     * Load grade_category data into the database, and adds the corresponding objects to this class' variable.
+     * category structure:
+                              course category
+                                    |
+                           +--------+-------------+
+                           |                      |
+             unittestcategory1               level1category
+                  |
+         +--------+-------------+
+         |                      |
+        unittestcategory2  unittestcategory3
+     */
+    private function load_grade_categories() {
+        global $DB;
+
+        $course_category = grade_category::fetch_course_category($this->course->id);
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'unittestcategory1';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $course_category->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 2;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = '/'.$course_category->id.'/'.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[0] = $grade_category;
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'unittestcategory2';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $this->grade_categories[0]->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 3;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = $this->grade_categories[0]->path.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[1] = $grade_category;
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'unittestcategory3';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $this->grade_categories[0]->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 3;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = $this->grade_categories[0]->path.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[2] = $grade_category;
+
+        // A category with no parent, but grade_items as children
+
+        $grade_category = new stdClass();
+
+        $grade_category->fullname    = 'level1category';
+        $grade_category->courseid    = $this->course->id;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 0;
+        $grade_category->droplow     = 0;
+        $grade_category->parent      = $course_category->id;
+        $grade_category->timecreated = time();
+        $grade_category->timemodified = time();
+        $grade_category->depth = 2;
+
+        $grade_category->id = $DB->insert_record('grade_categories', $grade_category);
+        $grade_category->path = '/'.$course_category->id.'/'.$grade_category->id.'/';
+        $DB->update_record('grade_categories', $grade_category);
+        $this->grade_categories[3] = $grade_category;
+    }
+
+    /**
+     * Load grade_item data into the database, and adds the corresponding objects to this class' variable.
+     */
+    protected function load_grade_items() {
+        global $DB;
+
+        $course_category = grade_category::fetch_course_category($this->course->id);
+
+        // id = 0
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitem1';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[0]->modname;
+        $grade_item->iteminstance = $this->course_module[0]->instance;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 30;
+        $grade_item->grademax = 110;
+        $grade_item->itemnumber = 1;
+        $grade_item->idnumber = 'item id 0';
+        $grade_item->iteminfo = 'Grade item 0 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 3;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[0] = $grade_item;
+
+        // id = 1
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitem2';
+        $grade_item->itemtype = 'import';
+        $grade_item->itemmodule = $this->course_module[1]->modname;
+        $grade_item->iteminstance = $this->course_module[1]->instance;
+        $grade_item->calculation = '= ##gi'.$this->grade_items[0]->id.'## + 30 + [[item id 0]] - [[item id 0]]';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->itemnumber = null;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 1 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 4;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[1] = $grade_item;
+
+        // id = 2
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[2]->id;
+        $grade_item->itemname = 'unittestgradeitem3';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[2]->modname;
+        $grade_item->iteminstance = $this->course_module[2]->instance;
+        $grade_item->gradetype = GRADE_TYPE_SCALE;
+        $grade_item->scaleid = $this->scale[0]->id;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = $this->scalemax[0];
+        $grade_item->iteminfo = 'Grade item 2 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 6;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[2] = $grade_item;
+
+        // Load grade_items associated with the 3 categories
+        // id = 3
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->iteminstance = $this->grade_categories[0]->id;
+        $grade_item->itemname = 'unittestgradeitemcategory1';
+        $grade_item->needsupdate = 0;
+        $grade_item->itemtype = 'category';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 3 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 1;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[3] = $grade_item;
+
+        // id = 4
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->iteminstance = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitemcategory2';
+        $grade_item->itemtype = 'category';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->needsupdate = 0;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 4 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 2;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[4] = $grade_item;
+
+        // id = 5
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->iteminstance = $this->grade_categories[2]->id;
+        $grade_item->itemname = 'unittestgradeitemcategory3';
+        $grade_item->itemtype = 'category';
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->needsupdate = true;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 5 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 5;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[5] = $grade_item;
+
+        // Orphan grade_item
+        // id = 6
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $course_category->id;
+        $grade_item->itemname = 'unittestorphangradeitem1';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[4]->modname;
+        $grade_item->iteminstance = $this->course_module[4]->instance;
+        $grade_item->itemnumber = 0;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 10;
+        $grade_item->grademax = 120;
+        $grade_item->locked = time();
+        $grade_item->iteminfo = 'Orphan Grade 6 item used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 7;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[6] = $grade_item;
+
+        // 2 grade items under level1category
+        // id = 7
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[3]->id;
+        $grade_item->itemname = 'singleparentitem1';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[5]->modname;
+        $grade_item->iteminstance = $this->course_module[5]->instance;
+        $grade_item->gradetype = GRADE_TYPE_SCALE;
+        $grade_item->scaleid = $this->scale[0]->id;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = $this->scalemax[0];
+        $grade_item->iteminfo = 'Grade item 7 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 9;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[7] = $grade_item;
+
+        // id = 8
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $this->grade_categories[3]->id;
+        $grade_item->itemname = 'singleparentitem2';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = $this->course_module[6]->modname;
+        $grade_item->iteminstance = $this->course_module[6]->instance;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Grade item 8 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 10;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[8] = $grade_item;
+
+        // Grade_item for level1category
+        // id = 9
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->itemname = 'grade_item for level1 category';
+        $grade_item->itemtype = 'category';
+        $grade_item->iteminstance = $this->grade_categories[3]->id;
+        $grade_item->needsupdate = true;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Orphan Grade item 9 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+        $grade_item->sortorder = 8;
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[9] = $grade_item;
+
+        // Manual grade_item
+        // id = 10
+        $grade_item = new stdClass();
+
+        $grade_item->courseid = $this->course->id;
+        $grade_item->categoryid = $course_category->id;
+        $grade_item->itemname = 'manual grade_item';
+        $grade_item->itemtype = 'manual';
+        $grade_item->itemnumber = 0;
+        $grade_item->needsupdate = false;
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->iteminfo = 'Manual grade item 10 used for unit testing';
+        $grade_item->timecreated = time();
+        $grade_item->timemodified = time();
+
+        $grade_item->id = $DB->insert_record('grade_items', $grade_item);
+        $this->grade_items[10] = $grade_item;
+    }
+
+    /**
+     * Load grade_grades data into the database, and adds the corresponding objects to this class' variable.
+     */
+    private function load_grade_grades() {
+        global $DB;
+
+        //this method is called once for each test method. Avoid adding things to $this->grade_grades multiple times
+        $this->grade_grades = array();
+
+        // Grades for grade_item 1
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[0]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 15; // too small
+        $grade->finalgrade = 30;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '1 of 17 grade_grades';
+        $grade->informationformat = FORMAT_PLAIN;
+        $grade->feedback = 'Good, but not good enough..';
+        $grade->feedbackformat = FORMAT_PLAIN;
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[0] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[0]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 40;
+        $grade->finalgrade = 40;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '2 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[1] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[0]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 170; // too big
+        $grade->finalgrade = 110;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '3 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[2] = $grade;
+
+
+        // No raw grades for grade_item 2 - it is calculated
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[1]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->finalgrade = 60;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '4 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[3] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[1]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->finalgrade = 70;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '5 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[4] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[1]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->finalgrade = 100;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '6 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[5] = $grade;
+
+
+        // Grades for grade_item 3
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[2]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 2;
+        $grade->finalgrade = 6;
+        $grade->scaleid = $this->scale[3]->id;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '7 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[6] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[2]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 3;
+        $grade->finalgrade = 2;
+        $grade->scaleid = $this->scale[3]->id;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '8 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[2]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 1;
+        $grade->finalgrade = 3;
+        $grade->scaleid = $this->scale[3]->id;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '9 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        // Grades for grade_item 7
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[6]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 97;
+        $grade->finalgrade = 69;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '10 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[6]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 49;
+        $grade->finalgrade = 87;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '11 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[6]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 67;
+        $grade->finalgrade = 94;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '12 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        // Grades for grade_item 8
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[7]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 3;
+        $grade->finalgrade = 3;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '13 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[7]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 6;
+        $grade->finalgrade = 6;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '14 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        // Grades for grade_item 9
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[8]->id;
+        $grade->userid = $this->user[1]->id;
+        $grade->rawgrade = 20;
+        $grade->finalgrade = 20;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '15 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[8]->id;
+        $grade->userid = $this->user[2]->id;
+        $grade->rawgrade = 50;
+        $grade->finalgrade = 50;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '16 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+
+        $grade = new stdClass();
+        $grade->itemid = $this->grade_items[8]->id;
+        $grade->userid = $this->user[3]->id;
+        $grade->rawgrade = 100;
+        $grade->finalgrade = 100;
+        $grade->timecreated = time();
+        $grade->timemodified = time();
+        $grade->information = '17 of 17 grade_grades';
+
+        $grade->id = $DB->insert_record('grade_grades', $grade);
+        $this->grade_grades[] = $grade;
+    }
+
+    /**
+     * Load grade_outcome data into the database, and adds the corresponding objects to this class' variable.
+     */
+    private function load_grade_outcomes() {
+        global $DB;
+
+        //this method is called once for each test method. Avoid adding things to $this->grade_outcomes multiple times
+        $this->grade_outcomes = array();
+
+        // Calculation for grade_item 1
+        $grade_outcome = new stdClass();
+        $grade_outcome->fullname = 'Team work';
+        $grade_outcome->shortname = 'Team work';
+        $grade_outcome->fullname = 'Team work outcome';
+        $grade_outcome->timecreated = time();
+        $grade_outcome->timemodified = time();
+        $grade_outcome->scaleid = $this->scale[2]->id;
+
+        $grade_outcome->id = $DB->insert_record('grade_outcomes', $grade_outcome);
+        $this->grade_outcomes[] = $grade_outcome;
+
+        // Calculation for grade_item 2
+        $grade_outcome = new stdClass();
+        $grade_outcome->fullname = 'Complete circuit board';
+        $grade_outcome->shortname = 'Complete circuit board';
+        $grade_outcome->fullname = 'Complete circuit board';
+        $grade_outcome->timecreated = time();
+        $grade_outcome->timemodified = time();
+        $grade_outcome->scaleid = $this->scale[3]->id;
+
+        $grade_outcome->id = $DB->insert_record('grade_outcomes', $grade_outcome);
+        $this->grade_outcomes[] = $grade_outcome;
+
+        // Calculation for grade_item 3
+        $grade_outcome = new stdClass();
+        $grade_outcome->fullname = 'Debug Java program';
+        $grade_outcome->shortname = 'Debug Java program';
+        $grade_outcome->fullname = 'Debug Java program';
+        $grade_outcome->timecreated = time();
+        $grade_outcome->timemodified = time();
+        $grade_outcome->scaleid = $this->scale[4]->id;
+
+        $grade_outcome->id = $DB->insert_record('grade_outcomes', $grade_outcome);
+        $this->grade_outcomes[] = $grade_outcome;
+    }
+}
+
+
diff --git a/lib/grade/tests/grade_category_test.php b/lib/grade/tests/grade_category_test.php
new file mode 100644 (file)
index 0000000..102ef86
--- /dev/null
@@ -0,0 +1,646 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_category_testcase extends grade_base_testcase {
+
+    public function test_grade_category() {
+        $this->sub_test_grade_category_construct();
+        $this->sub_test_grade_category_build_path();
+        $this->sub_test_grade_category_fetch();
+        $this->sub_test_grade_category_fetch_all();
+        $this->sub_test_grade_category_update();
+        $this->sub_test_grade_category_delete();
+        $this->sub_test_grade_category_insert();
+        $this->sub_test_grade_category_qualifies_for_regrading();
+        $this->sub_test_grade_category_force_regrading();
+        $this->sub_test_grade_category_aggregate_grades();
+        $this->sub_test_grade_category_apply_limit_rules();
+        $this->sub_test_grade_category_is_aggregationcoef_used();
+        $this->sub_test_grade_category_fetch_course_tree();
+        $this->sub_test_grade_category_get_children();
+        $this->sub_test_grade_category_load_grade_item();
+        $this->sub_test_grade_category_get_grade_item();
+        $this->sub_test_grade_category_load_parent_category();
+        $this->sub_test_grade_category_get_parent_category();
+        $this->sub_test_grade_category_get_name();
+        $this->sub_test_grade_category_set_parent();
+        $this->sub_test_grade_category_get_final();
+        $this->sub_test_grade_category_get_sortorder();
+        $this->sub_test_grade_category_set_sortorder();
+        $this->sub_test_grade_category_is_editable();
+        $this->sub_test_grade_category_move_after_sortorder();
+        $this->sub_test_grade_category_is_course_category();
+        $this->sub_test_grade_category_fetch_course_category();
+        $this->sub_test_grade_category_is_locked();
+        $this->sub_test_grade_category_set_locked();
+        $this->sub_test_grade_category_is_hidden();
+        $this->sub_test_grade_category_set_hidden();
+
+        //this won't work until MDL-11837 is complete
+        //$this->sub_test_grade_category_generate_grades();
+
+        //do this last as adding a second course category messes up the data
+        $this->sub_test_grade_category_insert_course_category();
+    }
+
+    //adds 3 new grade categories at various depths
+    protected function sub_test_grade_category_construct() {
+        $course_category = grade_category::fetch_course_category($this->courseid);
+
+        $params = new stdClass();
+
+        $params->courseid = $this->courseid;
+        $params->fullname = 'unittestcategory4';
+
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+        $this->grade_categories[] = $grade_category;
+
+        $this->assertEquals($params->courseid, $grade_category->courseid);
+        $this->assertEquals($params->fullname, $grade_category->fullname);
+        $this->assertEquals(2, $grade_category->depth);
+        $this->assertEquals("/$course_category->id/$grade_category->id/", $grade_category->path);
+        $parentpath = $grade_category->path;
+
+        // Test a child category
+        $params->parent = $grade_category->id;
+        $params->fullname = 'unittestcategory5';
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+        $this->grade_categories[] = $grade_category;
+
+        $this->assertEquals(3, $grade_category->depth);
+        $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
+        $parentpath = $grade_category->path;
+
+        // Test a third depth category
+        $params->parent = $grade_category->id;
+        $params->fullname = 'unittestcategory6';
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+        $this->grade_categories[50] = $grade_category;//going to delete this one later hence the special index
+
+        $this->assertEquals(4, $grade_category->depth);
+        $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
+    }
+
+    protected function sub_test_grade_category_build_path() {
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($grade_category, 'build_path'));
+        $path = grade_category::build_path($grade_category);
+        $this->assertEquals($grade_category->path, $path);
+    }
+
+    protected function sub_test_grade_category_fetch() {
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'fetch'));
+
+        $grade_category = grade_category::fetch(array('id'=>$this->grade_categories[0]->id));
+        $this->assertEquals($this->grade_categories[0]->id, $grade_category->id);
+        $this->assertEquals($this->grade_categories[0]->fullname, $grade_category->fullname);
+    }
+
+    protected function sub_test_grade_category_fetch_all() {
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'fetch_all'));
+
+        $grade_categories = grade_category::fetch_all(array('courseid'=>$this->courseid));
+        $this->assertEquals(count($this->grade_categories), count($grade_categories)-1);
+    }
+
+    protected function sub_test_grade_category_update() {
+        global $DB;
+        $grade_category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($grade_category, 'update'));
+
+        $grade_category->fullname = 'Updated info for this unittest grade_category';
+        $grade_category->path = null; // path must be recalculated if missing
+        $grade_category->depth = null;
+        $grade_category->aggregation = GRADE_AGGREGATE_MAX; // should force regrading
+
+        $grade_item = $grade_category->get_grade_item();
+        $this->assertEquals(0, $grade_item->needsupdate);
+
+        $this->assertTrue($grade_category->update());
+
+        $fullname = $DB->get_field('grade_categories', 'fullname', array('id' => $this->grade_categories[0]->id));
+        $this->assertEquals($grade_category->fullname, $fullname);
+
+        $path = $DB->get_field('grade_categories', 'path', array('id' => $this->grade_categories[0]->id));
+        $this->assertEquals($grade_category->path, $path);
+
+        $depth = $DB->get_field('grade_categories', 'depth', array('id' => $this->grade_categories[0]->id));
+        $this->assertEquals($grade_category->depth, $depth);
+
+        $grade_item = $grade_category->get_grade_item();
+        $this->assertEquals(1, $grade_item->needsupdate);
+    }
+
+    protected function sub_test_grade_category_delete() {
+        global $DB;
+
+        $grade_category = new grade_category($this->grade_categories[50]);
+        $this->assertTrue(method_exists($grade_category, 'delete'));
+
+        $this->assertTrue($grade_category->delete());
+        $this->assertFalse($DB->get_record('grade_categories', array('id' => $grade_category->id)));
+    }
+
+    protected function sub_test_grade_category_insert() {
+        $course_category = grade_category::fetch_course_category($this->courseid);
+
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'insert'));
+
+        $grade_category->fullname    = 'unittestcategory4';
+        $grade_category->courseid    = $this->courseid;
+        $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
+        $grade_category->aggregateonlygraded = 1;
+        $grade_category->keephigh    = 100;
+        $grade_category->droplow     = 10;
+        $grade_category->hidden      = 0;
+        $grade_category->parent      = $this->grade_categories[1]->id; //sub_test_grade_category_delete() removed the category at 0
+
+        $grade_category->insert();
+
+        $this->assertEquals('/'.$course_category->id.'/'.$this->grade_categories[1]->parent.'/'.$this->grade_categories[1]->id.'/'.$grade_category->id.'/', $grade_category->path);
+        $this->assertEquals(4, $grade_category->depth);
+
+        $last_grade_category = end($this->grade_categories);
+
+        $this->assertFalse(empty($grade_category->grade_item));
+        $this->assertEquals($grade_category->id, $grade_category->grade_item->iteminstance);
+        $this->assertEquals('category', $grade_category->grade_item->itemtype);
+
+        $this->assertEquals($grade_category->id, $last_grade_category->id + 1);
+        $this->assertFalse(empty($grade_category->timecreated));
+        $this->assertFalse(empty($grade_category->timemodified));
+    }
+
+    protected function sub_test_grade_category_qualifies_for_regrading() {
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($grade_category, 'qualifies_for_regrading'));
+        $this->assertFalse($grade_category->qualifies_for_regrading());
+
+        $grade_category->aggregation = GRADE_AGGREGATE_MAX;
+        $this->assertTrue($grade_category->qualifies_for_regrading());
+
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $grade_category->droplow = 99;
+        $this->assertTrue($grade_category->qualifies_for_regrading());
+
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $grade_category->keephigh = 99;
+        $this->assertTrue($grade_category->qualifies_for_regrading());
+    }
+
+    protected function sub_test_grade_category_force_regrading() {
+        $grade_category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($grade_category, 'force_regrading'));
+
+        $grade_category->load_grade_item();
+        $this->assertEquals(0, $grade_category->grade_item->needsupdate);
+
+        $grade_category->force_regrading();
+
+        $grade_category->grade_item = null;
+        $grade_category->load_grade_item();
+
+        $this->assertEquals(1, $grade_category->grade_item->needsupdate);
+    }
+
+    /**
+     * Tests the calculation of grades using the various aggregation methods with and without hidden grades
+     * This will not work entirely until MDL-11837 is done
+     * @global type $DB
+     */
+    protected function sub_test_grade_category_generate_grades() {
+        global $DB;
+
+        //inserting some special grade items to make testing the final grade calculation easier
+        $params->courseid = $this->courseid;
+        $params->fullname = 'unittestgradecalccategory';
+        $params->aggregation = GRADE_AGGREGATE_MEAN;
+        $params->aggregateonlygraded = 0;
+        $grade_category = new grade_category($params, false);
+        $grade_category->insert();
+
+        $this->assertTrue(method_exists($grade_category, 'generate_grades'));
+
+        $grade_category->load_grade_item();
+        $cgi = $grade_category->get_grade_item();
+        $cgi->grademin = 0;
+        $cgi->grademax = 20;//3 grade items out of 10 but category is out of 20 to force scaling to occur
+        $cgi->update();
+
+        //3 grade items each with a maximum grade of 10
+        $grade_items = array();
+        for ($i=0; $i<3; $i++) {
+            $grade_items[$i] = new grade_item();
+            $grade_items[$i]->courseid = $this->courseid;
+            $grade_items[$i]->categoryid = $grade_category->id;
+            $grade_items[$i]->itemname = 'manual grade_item '.$i;
+            $grade_items[$i]->itemtype = 'manual';
+            $grade_items[$i]->itemnumber = 0;
+            $grade_items[$i]->needsupdate = false;
+            $grade_items[$i]->gradetype = GRADE_TYPE_VALUE;
+            $grade_items[$i]->grademin = 0;
+            $grade_items[$i]->grademax = 10;
+            $grade_items[$i]->iteminfo = 'Manual grade item used for unit testing';
+            $grade_items[$i]->timecreated = time();
+            $grade_items[$i]->timemodified = time();
+
+            //used as the weight by weighted mean and as extra credit by mean with extra credit
+            //Will be 0, 1 and 2
+            $grade_items[$i]->aggregationcoef = $i;
+
+            $grade_items[$i]->insert();
+        }
+
+        //a grade for each grade item
+        $grade_grades = array();
+        for ($i=0; $i<3; $i++) {
+            $grade_grades[$i] = new grade_grade();
+            $grade_grades[$i]->itemid = $grade_items[$i]->id;
+            $grade_grades[$i]->userid = $this->userid;
+            $grade_grades[$i]->rawgrade = ($i+1)*2;//produce grade grades of 2, 4 and 6
+            $grade_grades[$i]->finalgrade = ($i+1)*2;
+            $grade_grades[$i]->timecreated = time();
+            $grade_grades[$i]->timemodified = time();
+            $grade_grades[$i]->information = '1 of 2 grade_grades';
+            $grade_grades[$i]->informationformat = FORMAT_PLAIN;
+            $grade_grades[$i]->feedback = 'Good, but not good enough..';
+            $grade_grades[$i]->feedbackformat = FORMAT_PLAIN;
+
+            $grade_grades[$i]->insert();
+        }
+
+        //3 grade items with 1 grade_grade each.
+        //grade grades have the values 2, 4 and 6
+
+        //First correct answer is the aggregate with all 3 grades
+        //Second correct answer is with the first grade (value 2) hidden
+
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEDIAN, 'GRADE_AGGREGATE_MEDIAN', 8, 8);
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MAX, 'GRADE_AGGREGATE_MAX', 12, 12);
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MODE, 'GRADE_AGGREGATE_MODE', 12, 12);
+
+        //weighted mean. note grade totals are rounded to an int to prevent rounding discrepancies. correct final grade isnt actually exactly 10
+        //3 items with grades 2, 4 and 6 with weights 0, 1 and 2 and all out of 10. then doubled to be out of 20.
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN, 'GRADE_AGGREGATE_WEIGHTED_MEAN', 10, 10);
+
+        //simple weighted mean
+        //3 items with grades 2, 4 and 6 equally weighted and all out of 10. then doubled to be out of 20.
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN2, 'GRADE_AGGREGATE_WEIGHTED_MEAN2', 8, 10);
+
+        //mean of grades with extra credit
+        //3 items with grades 2, 4 and 6 with extra credit 0, 1 and 2 equally weighted and all out of 10. then doubled to be out of 20.
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_EXTRACREDIT_MEAN, 'GRADE_AGGREGATE_EXTRACREDIT_MEAN', 10, 13);
+
+        //aggregation tests the are affected by a hidden grade currently dont work as we dont store the altered grade in the database
+        //instead an in memory recalculation is done. This should be remedied by MDL-11837
+
+        //fails with 1 grade hidden. still reports 8 as being correct
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEAN, 'GRADE_AGGREGATE_MEAN', 8, 10);
+
+        //fails with 1 grade hidden. still reports 4 as being correct
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MIN, 'GRADE_AGGREGATE_MIN', 4, 8);
+
+        //fails with 1 grade hidden. still reports 12 as being correct
+        $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_SUM, 'GRADE_AGGREGATE_SUM', 12, 10);
+    }
+
+    /**
+     * Test grade category aggregation using the supplied grade objects and aggregation method
+     * @param grade_category $grade_category the category to be tested
+     * @param array $grade_items array of instance of grade_item
+     * @param array $grade_grades array of instances of grade_grade
+     * @param int $aggmethod the aggregation method to apply ie GRADE_AGGREGATE_MEAN
+     * @param string $aggmethodname the name of the aggregation method to apply. Used to display any test failure messages
+     * @param int $correct1 the correct final grade for the category with NO items hidden
+     * @param int $correct2 the correct final grade for the category with the grade at $grade_grades[0] hidden
+     * @return void
+    */
+    protected function helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, $aggmethod, $aggmethodname, $correct1, $correct2) {
+        global $DB;
+
+        $grade_category->aggregation = $aggmethod;
+        $grade_category->update();
+
+        //check grade_item isnt hidden from a previous test
+        $grade_items[0]->set_hidden(0, true);
+        $this->helper_test_grade_aggregation_result($grade_category, $correct1, 'Testing aggregation method('.$aggmethodname.') with no items hidden %s');
+
+        //hide the grade item with grade of 2
+        $grade_items[0]->set_hidden(1, true);
+        $this->helper_test_grade_aggregation_result($grade_category, $correct2, 'Testing aggregation method('.$aggmethodname.') with 1 item hidden %s');
+    }
+
+    /**
+     * Verify the value of the category grade item for $this->userid
+     * @param grade_category $grade_category the category to be tested
+     * @param int $correctgrade the expected grade
+     * @param string msg The message that should be displayed if the correct grade is not found
+     * @return void
+     */
+    protected function helper_test_grade_aggregation_result($grade_category, $correctgrade, $msg) {
+        global $DB;
+
+        $category_grade_item = $grade_category->get_grade_item();
+
+        //this creates all the grade_grades we need
+        grade_regrade_final_grades($this->courseid);
+
+        $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
+        $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
+        $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
+
+        /*
+         * TODO this doesnt work as the grade_grades created by $grade_category->generate_grades(); dont
+         * observe the category's max grade
+        //delete the grade_grades for the category itself and check they get recreated correctly
+        $DB->delete_records('grade_grades', array('itemid'=>$category_grade_item->id));
+        $grade_category->generate_grades();
+
+        $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
+        $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
+        $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
+         *
+         */
+    }
+
+    protected function sub_test_grade_category_aggregate_grades() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'aggregate_grades'));
+        // tested more fully via test_grade_category_generate_grades()
+    }
+
+    protected function sub_test_grade_category_apply_limit_rules() {
+        $items[$this->grade_items[0]->id] = new grade_item($this->grade_items[0], false);
+        $items[$this->grade_items[1]->id] = new grade_item($this->grade_items[1], false);
+        $items[$this->grade_items[2]->id] = new grade_item($this->grade_items[2], false);
+        $items[$this->grade_items[4]->id] = new grade_item($this->grade_items[4], false);
+
+        $category = new grade_category();
+        $category->droplow = 2;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 2);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
+        $this->assertEquals($grades[$this->grade_items[4]->id], 7.3754);
+
+        $category = new grade_category();
+        $category->keephigh = 1;
+        $category->droplow = 0;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 1);
+        $grade = reset($grades);
+        $this->assertEquals(9.4743, $grade);
+
+        $category = new grade_category();
+        $category->droplow     = 2;
+        $category->aggregation = GRADE_AGGREGATE_SUM;
+        $items[$this->grade_items[2]->id]->aggregationcoef = 1;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 2);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
+        $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
+
+        $category = new grade_category();
+        $category->keephigh = 1;
+        $category->droplow = 0;
+        $category->aggregation = GRADE_AGGREGATE_SUM;
+        $items[$this->grade_items[2]->id]->aggregationcoef = 1;
+        $grades = array($this->grade_items[0]->id=>5.374,
+                        $this->grade_items[1]->id=>9.4743,
+                        $this->grade_items[2]->id=>2.5474,
+                        $this->grade_items[4]->id=>7.3754);
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 2);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
+        $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
+    }
+
+    /**
+     * TODO implement
+     */
+    protected function sub_test_grade_category_is_aggregationcoef_used() {
+
+    }
+
+    protected function sub_test_grade_category_fetch_course_tree() {
+        $category = new grade_category();
+        $this->assertTrue(method_exists($category, 'fetch_course_tree'));
+        //TODO: add some tests
+    }
+
+    protected function sub_test_grade_category_get_children() {
+        $course_category = grade_category::fetch_course_category($this->courseid);
+
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_children'));
+
+        $children_array = $category->get_children(0);
+
+        $this->assertTrue(is_array($children_array));
+        $this->assertFalse(empty($children_array[2]));
+        $this->assertFalse(empty($children_array[2]['object']));
+        $this->assertFalse(empty($children_array[2]['children']));
+        $this->assertEquals($this->grade_categories[1]->id, $children_array[2]['object']->id);
+        $this->assertEquals($this->grade_categories[2]->id, $children_array[5]['object']->id);
+        $this->assertEquals($this->grade_items[0]->id, $children_array[2]['children'][3]['object']->id);
+        $this->assertEquals($this->grade_items[1]->id, $children_array[2]['children'][4]['object']->id);
+        $this->assertEquals($this->grade_items[2]->id, $children_array[5]['children'][6]['object']->id);
+    }
+
+    protected function sub_test_grade_category_load_grade_item() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'load_grade_item'));
+        $this->assertEquals(null, $category->grade_item);
+        $category->load_grade_item();
+        $this->assertEquals($this->grade_items[3]->id, $category->grade_item->id);
+    }
+
+    protected function sub_test_grade_category_get_grade_item() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_grade_item'));
+        $grade_item = $category->get_grade_item();
+        $this->assertEquals($this->grade_items[3]->id, $grade_item->id);
+    }
+
+    protected function sub_test_grade_category_load_parent_category() {
+        $category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($category, 'load_parent_category'));
+        $this->assertEquals(null, $category->parent_category);
+        $category->load_parent_category();
+        $this->assertEquals($this->grade_categories[0]->id, $category->parent_category->id);
+    }
+
+    protected function sub_test_grade_category_get_parent_category() {
+        $category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($category, 'get_parent_category'));
+        $parent_category = $category->get_parent_category();
+        $this->assertEquals($this->grade_categories[0]->id, $parent_category->id);
+    }
+
+    protected function sub_test_grade_category_get_name() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_name'));
+        $this->assertEquals($this->grade_categories[0]->fullname, $category->get_name());
+    }
+
+    protected function sub_test_grade_category_set_parent() {
+        $category = new grade_category($this->grade_categories[1]);
+        $this->assertTrue(method_exists($category, 'set_parent'));
+        // TODO: implement detailed tests
+
+        $course_category = grade_category::fetch_course_category($this->courseid);
+        $this->assertTrue($category->set_parent($course_category->id));
+        $this->assertEquals($course_category->id, $category->parent);
+    }
+
+    protected function sub_test_grade_category_get_final() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_final'));
+        $category->load_grade_item();
+        $this->assertEquals($category->get_final(), $category->grade_item->get_final());
+    }
+
+    protected function sub_test_grade_category_get_sortorder() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'get_sortorder'));
+        $category->load_grade_item();
+        $this->assertEquals($category->get_sortorder(), $category->grade_item->get_sortorder());
+    }
+
+    protected function sub_test_grade_category_set_sortorder() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'set_sortorder'));
+        $category->load_grade_item();
+        $this->assertEquals($category->set_sortorder(10), $category->grade_item->set_sortorder(10));
+    }
+
+    protected function sub_test_grade_category_move_after_sortorder() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'move_after_sortorder'));
+        $category->load_grade_item();
+        $this->assertEquals($category->move_after_sortorder(10), $category->grade_item->move_after_sortorder(10));
+    }
+
+    protected function sub_test_grade_category_is_course_category() {
+        $category = grade_category::fetch_course_category($this->courseid);
+        $this->assertTrue(method_exists($category, 'is_course_category'));
+        $this->assertTrue($category->is_course_category());
+    }
+
+    protected function sub_test_grade_category_fetch_course_category() {
+        $category = new grade_category();
+        $this->assertTrue(method_exists($category, 'fetch_course_category'));
+        $category = grade_category::fetch_course_category($this->courseid);
+        $this->assertTrue(empty($category->parent));
+    }
+    /**
+     * TODO implement
+     */
+    protected function sub_test_grade_category_is_editable() {
+
+    }
+
+    protected function sub_test_grade_category_is_locked() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'is_locked'));
+        $category->load_grade_item();
+        $this->assertEquals($category->is_locked(), $category->grade_item->is_locked());
+    }
+
+    protected function sub_test_grade_category_set_locked() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'set_locked'));
+
+        //will return false as cannot lock a grade that needs updating
+        $this->assertFalse($category->set_locked(1));
+        grade_regrade_final_grades($this->courseid);
+
+        //get the category from the db again
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue($category->set_locked(1));
+    }
+
+    protected function sub_test_grade_category_is_hidden() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'is_hidden'));
+        $category->load_grade_item();
+        $this->assertEquals($category->is_hidden(), $category->grade_item->is_hidden());
+    }
+
+    protected function sub_test_grade_category_set_hidden() {
+        $category = new grade_category($this->grade_categories[0]);
+        $this->assertTrue(method_exists($category, 'set_hidden'));
+        $category->set_hidden(1);
+        $category->load_grade_item();
+        $this->assertEquals(true, $category->grade_item->is_hidden());
+    }
+
+    //beware: adding a duplicate course category messes up the data in a way that's hard to recover from
+    protected function sub_test_grade_category_insert_course_category() {
+        $grade_category = new grade_category();
+        $this->assertTrue(method_exists($grade_category, 'insert_course_category'));
+
+        $id = $grade_category->insert_course_category($this->courseid);
+        $this->assertNotNull($id);
+        $this->assertEquals('?', $grade_category->fullname);
+        $this->assertEquals(GRADE_AGGREGATE_WEIGHTED_MEAN2, $grade_category->aggregation);
+        $this->assertEquals("/$id/", $grade_category->path);
+        $this->assertEquals(1, $grade_category->depth);
+        $this->assertNull($grade_category->parent);
+    }
+
+    protected function generate_random_raw_grade($item, $userid) {
+        $grade = new grade_grade();
+        $grade->itemid = $item->id;
+        $grade->userid = $userid;
+        $grade->grademin = 0;
+        $grade->grademax = 1;
+        $valuetype = "grade$item->gradetype";
+        $grade->rawgrade = rand(0, 1000) / 1000;
+        $grade->insert();
+        return $grade->rawgrade;
+    }
+}
diff --git a/lib/grade/tests/grade_grade_test.php b/lib/grade/tests/grade_grade_test.php
new file mode 100644 (file)
index 0000000..56dddc8
--- /dev/null
@@ -0,0 +1,196 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_grade_testcase extends grade_base_testcase {
+
+    public function test_grade_grade() {
+        $this->sub_test_grade_grade_construct();
+        $this->sub_test_grade_grade_insert();
+        $this->sub_test_grade_grade_update();
+        $this->sub_test_grade_grade_fetch();
+        $this->sub_test_grade_grade_fetch_all();
+        $this->sub_test_grade_grade_load_grade_item();
+        $this->sub_test_grade_grade_standardise_score();
+        $this->sub_test_grade_grade_is_locked();
+        $this->sub_test_grade_grade_set_hidden();
+        $this->sub_test_grade_grade_is_hidden();
+    }
+
+    protected function sub_test_grade_grade_construct() {
+        $params = new stdClass();
+
+        $params->itemid = $this->grade_items[0]->id;
+        $params->userid = 1;
+        $params->rawgrade = 88;
+        $params->rawgrademax = 110;
+        $params->rawgrademin = 18;
+
+        $grade_grade = new grade_grade($params, false);
+        $this->assertEquals($params->itemid, $grade_grade->itemid);
+        $this->assertEquals($params->rawgrade, $grade_grade->rawgrade);
+    }
+
+    protected function sub_test_grade_grade_insert() {
+        $grade_grade = new grade_grade();
+        $this->assertTrue(method_exists($grade_grade, 'insert'));
+
+        $grade_grade->itemid = $this->grade_items[0]->id;
+        $grade_grade->userid = 10;
+        $grade_grade->rawgrade = 88;
+        $grade_grade->rawgrademax = 110;
+        $grade_grade->rawgrademin = 18;
+
+        // Check the grade_item's needsupdate variable first
+        $grade_grade->load_grade_item();
+        $this->assertEmpty($grade_grade->grade_item->needsupdate);
+
+        $grade_grade->insert();
+
+        $last_grade_grade = end($this->grade_grades);
+
+        $this->assertEquals($grade_grade->id, $last_grade_grade->id + 1);
+
+        // timecreated will only be set if the grade was submitted by an activity module
+        $this->assertTrue(empty($grade_grade->timecreated));
+        // timemodified will only be set if the grade was submitted by an activity module
+        $this->assertTrue(empty($grade_grade->timemodified));
+
+        //keep our collection the same as is in the database
+        $this->grade_grades[] = $grade_grade;
+    }
+
+    protected function sub_test_grade_grade_update() {
+        $grade_grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade_grade, 'update'));
+    }
+
+    protected function sub_test_grade_grade_fetch() {
+        $grade_grade = new grade_grade();
+        $this->assertTrue(method_exists($grade_grade, 'fetch'));
+
+        $grades = grade_grade::fetch(array('id'=>$this->grade_grades[0]->id));
+        $this->assertEquals($this->grade_grades[0]->id, $grades->id);
+        $this->assertEquals($this->grade_grades[0]->rawgrade, $grades->rawgrade);
+    }
+
+    protected function sub_test_grade_grade_fetch_all() {
+        $grade_grade = new grade_grade();
+        $this->assertTrue(method_exists($grade_grade, 'fetch_all'));
+
+        $grades = grade_grade::fetch_all(array());
+        $this->assertEquals(count($this->grade_grades), count($grades));
+    }
+
+    protected function sub_test_grade_grade_load_grade_item() {
+        $grade_grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade_grade, 'load_grade_item'));
+        $this->assertNull($grade_grade->grade_item);
+        $this->assertNotEmpty($grade_grade->itemid);
+        $this->assertNotNull($grade_grade->load_grade_item());
+        $this->assertNotNull($grade_grade->grade_item);
+        $this->assertEquals($this->grade_items[0]->id, $grade_grade->grade_item->id);
+    }
+
+
+    protected function sub_test_grade_grade_standardise_score() {
+        $this->assertEquals(4, round(grade_grade::standardise_score(6, 0, 7, 0, 5)));
+        $this->assertEquals(40, grade_grade::standardise_score(50, 30, 80, 0, 100));
+    }
+
+
+    /*
+     * Disabling this test: the set_locked() arguments have been modified, rendering these tests useless until they are re-written
+
+    protected function test_grade_grade_set_locked() {
+        $grade_item = new grade_item($this->grade_items[0]);
+        $grade = new grade_grade($grade_item->get_final(1));
+        $this->assertTrue(method_exists($grade, 'set_locked'));
+
+        $this->assertTrue(empty($grade_item->locked));
+        $this->assertTrue(empty($grade->locked));
+
+        $this->assertTrue($grade->set_locked(true));
+        $this->assertFalse(empty($grade->locked));
+        $this->assertTrue($grade->set_locked(false));
+        $this->assertTrue(empty($grade->locked));
+
+        $this->assertTrue($grade_item->set_locked(true, true));
+        $grade = new grade_grade($grade_item->get_final(1));
+
+        $this->assertFalse(empty($grade->locked));
+        $this->assertFalse($grade->set_locked(true, false));
+
+        $this->assertTrue($grade_item->set_locked(true, false));
+        $grade = new grade_grade($grade_item->get_final(1));
+
+        $this->assertTrue($grade->set_locked(true, false));
+    }
+    */
+
+    protected function sub_test_grade_grade_is_locked() {
+        $grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade, 'is_locked'));
+
+        $this->assertFalse($grade->is_locked());
+        $grade->locked = time();
+        $this->assertTrue($grade->is_locked());
+    }
+
+    protected function sub_test_grade_grade_set_hidden() {
+        $grade = new grade_grade($this->grade_grades[0], false);
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade, 'set_hidden'));
+
+        $this->assertEquals(0, $grade_item->hidden);
+        $this->assertEquals(0, $grade->hidden);
+
+        $grade->set_hidden(0);
+        $this->assertEquals(0, $grade->hidden);
+
+        $grade->set_hidden(1);
+        $this->assertEquals(1, $grade->hidden);
+
+        $grade->set_hidden(0);
+        $this->assertEquals(0, $grade->hidden);
+    }
+
+    protected function sub_test_grade_grade_is_hidden() {
+        $grade = new grade_grade($this->grade_grades[0], false);
+        $this->assertTrue(method_exists($grade, 'is_hidden'));
+
+        $this->assertFalse($grade->is_hidden());
+        $grade->hidden = 1;
+        $this->assertTrue($grade->is_hidden());
+
+        $grade->hidden = time()-666;
+        $this->assertFalse($grade->is_hidden());
+
+        $grade->hidden = time()+666;
+        $this->assertTrue($grade->is_hidden());
+    }
+}
diff --git a/lib/grade/tests/grade_item_test.php b/lib/grade/tests/grade_item_test.php
new file mode 100644 (file)
index 0000000..dab82c2
--- /dev/null
@@ -0,0 +1,547 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+class grade_item_testcase extends grade_base_testcase {
+    public function test_grade_item() {
+        $this->sub_test_grade_item_construct();
+        $this->sub_test_grade_item_insert();
+        $this->sub_test_grade_item_delete();
+        $this->sub_test_grade_item_update();
+        $this->sub_test_grade_item_load_scale();
+        $this->sub_test_grade_item_load_outcome();
+        $this->sub_test_grade_item_qualifies_for_regrading();
+        $this->sub_test_grade_item_force_regrading();
+        $this->sub_test_grade_item_fetch();
+        $this->sub_test_grade_item_fetch_all();
+        $this->sub_test_grade_item_get_all_finals();
+        $this->sub_test_grade_item_get_final();
+        $this->sub_test_grade_item_get_sortorder();
+        $this->sub_test_grade_item_set_sortorder();
+        $this->sub_test_grade_item_move_after_sortorder();
+        $this->sub_test_grade_item_get_name();
+        $this->sub_test_grade_item_set_parent();
+        $this->sub_test_grade_item_get_parent_category();
+        $this->sub_test_grade_item_load_parent_category();
+        $this->sub_test_grade_item_get_item_category();
+        $this->sub_test_grade_item_load_item_category();
+        $this->sub_test_grade_item_regrade_final_grades();
+        $this->sub_test_grade_item_adjust_raw_grade();
+        $this->sub_test_grade_item_set_locked();
+        $this->sub_test_grade_item_is_locked();
+        $this->sub_test_grade_item_set_hidden();
+        $this->sub_test_grade_item_is_hidden();
+        $this->sub_test_grade_item_is_category_item();
+        $this->sub_test_grade_item_is_course_item();
+        $this->sub_test_grade_item_fetch_course_item();
+        $this->sub_test_grade_item_depends_on();
+        $this->sub_test_grade_item_is_calculated();
+        $this->sub_test_grade_item_set_calculation();
+        $this->sub_test_grade_item_get_calculation();
+        $this->sub_test_grade_item_compute();
+    }
+
+    protected function sub_test_grade_item_construct() {
+        $params = new stdClass();
+
+        $params->courseid = $this->courseid;
+        $params->categoryid = $this->grade_categories[1]->id;
+        $params->itemname = 'unittestgradeitem4';
+        $params->itemtype = 'mod';
+        $params->itemmodule = 'database';
+        $params->iteminfo = 'Grade item used for unit testing';
+
+        $grade_item = new grade_item($params, false);
+
+        $this->assertEquals($params->courseid, $grade_item->courseid);
+        $this->assertEquals($params->categoryid, $grade_item->categoryid);
+        $this->assertEquals($params->itemmodule, $grade_item->itemmodule);
+    }
+
+    protected function sub_test_grade_item_insert() {
+        $grade_item = new grade_item();
+        $this->assertTrue(method_exists($grade_item, 'insert'));
+
+        $grade_item->courseid = $this->courseid;
+        $grade_item->categoryid = $this->grade_categories[1]->id;
+        $grade_item->itemname = 'unittestgradeitem4';
+        $grade_item->itemtype = 'mod';
+        $grade_item->itemmodule = 'quiz';
+        $grade_item->iteminfo = 'Grade item used for unit testing';
+
+        $grade_item->insert();
+
+        $last_grade_item = end($this->grade_items);
+
+        $this->assertEquals($grade_item->id, $last_grade_item->id + 1);
+        $this->assertEquals(11, $grade_item->sortorder);
+
+        //keep our reference collection the same as what is in the database
+        $this->grade_items[] = $grade_item;
+    }
+
+    protected function sub_test_grade_item_delete() {
+        global $DB;
+        $grade_item = new grade_item($this->grade_items[7],false);//use a grade item not touched by previous (or future) tests
+        $this->assertTrue(method_exists($grade_item, 'delete'));
+
+        $this->assertTrue($grade_item->delete());
+
+        $this->assertFalse($DB->get_record('grade_items', array('id' => $grade_item->id)));
+
+        //keep our reference collection the same as the database
+        unset($this->grade_items[7]);
+    }
+
+    protected function sub_test_grade_item_update() {
+        global $DB;
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'update'));
+
+        $grade_item->iteminfo = 'Updated info for this unittest grade_item';
+
+        $this->assertTrue($grade_item->update());
+
+        $grade_item->grademin = 14;
+        $this->assertTrue($grade_item->qualifies_for_regrading());
+        $this->assertTrue($grade_item->update());
+
+        $iteminfo = $DB->get_field('grade_items', 'iteminfo', array('id' => $this->grade_items[0]->id));
+        $this->assertEquals($grade_item->iteminfo, $iteminfo);
+    }
+
+    protected function sub_test_grade_item_load_scale() {
+        $grade_item = new grade_item($this->grade_items[2], false);
+        $this->assertTrue(method_exists($grade_item, 'load_scale'));
+        $scale = $grade_item->load_scale();
+        $this->assertFalse(empty($grade_item->scale));
+        $this->assertEquals($scale->id, $this->grade_items[2]->scaleid);
+    }
+
+    protected function sub_test_grade_item_load_outcome() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'load_outcome'));
+        //TODO: add tests
+    }
+
+    protected function sub_test_grade_item_qualifies_for_regrading() {
+        $grade_item = new grade_item($this->grade_items[3], false);//use a grade item not touched by previous tests
+        $this->assertTrue(method_exists($grade_item, 'qualifies_for_regrading'));
+
+        $this->assertFalse($grade_item->qualifies_for_regrading());
+
+        $grade_item->iteminfo = 'Updated info for this unittest grade_item';
+
+        $this->assertFalse($grade_item->qualifies_for_regrading());
+
+        $grade_item->grademin = 14;
+
+        $this->assertTrue($grade_item->qualifies_for_regrading());
+    }
+
+    protected function sub_test_grade_item_force_regrading() {
+        $grade_item = new grade_item($this->grade_items[3], false);//use a grade item not touched by previous tests
+        $this->assertTrue(method_exists($grade_item, 'force_regrading'));
+
+        $this->assertEquals(0, $grade_item->needsupdate);
+
+        $grade_item->force_regrading();
+        $this->assertEquals(1, $grade_item->needsupdate);
+        $grade_item->update_from_db();
+        $this->assertEquals(1, $grade_item->needsupdate);
+    }
+
+    protected function sub_test_grade_item_fetch() {
+        $grade_item = new grade_item();
+        $this->assertTrue(method_exists($grade_item, 'fetch'));
+
+        //not using $this->grade_items[0] as it's iteminfo was modified by sub_test_grade_item_qualifies_for_regrading()
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[1]->id));
+        $this->assertEquals($this->grade_items[1]->id, $grade_item->id);
+        $this->assertEquals($this->grade_items[1]->iteminfo, $grade_item->iteminfo);
+
+        $grade_item = grade_item::fetch(array('itemtype'=>$this->grade_items[1]->itemtype, 'itemmodule'=>$this->grade_items[1]->itemmodule));
+        $this->assertEquals($this->grade_items[1]->id, $grade_item->id);
+        $this->assertEquals($this->grade_items[1]->iteminfo, $grade_item->iteminfo);
+    }
+
+    protected function sub_test_grade_item_fetch_all() {
+        $grade_item = new grade_item();
+        $this->assertTrue(method_exists($grade_item, 'fetch_all'));
+
+        $grade_items = grade_item::fetch_all(array('courseid'=>$this->courseid));
+        $this->assertEquals(count($this->grade_items), count($grade_items)-1);//-1 to account for the course grade item
+    }
+
+    // Retrieve all final scores for a given grade_item.
+    protected function sub_test_grade_item_get_all_finals() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_final'));
+
+        $final_grades = $grade_item->get_final();
+        $this->assertEquals(3, count($final_grades));
+    }
+
+
+    // Retrieve all final scores for a specific userid.
+    protected function sub_test_grade_item_get_final() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_final'));
+        $final_grade = $grade_item->get_final($this->user[1]->id);
+        $this->assertEquals($this->grade_grades[0]->finalgrade, $final_grade->finalgrade);
+    }
+
+    protected function sub_test_grade_item_get_sortorder() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_sortorder'));
+        $sortorder = $grade_item->get_sortorder();
+        $this->assertEquals($this->grade_items[0]->sortorder, $sortorder);
+    }
+
+    protected function sub_test_grade_item_set_sortorder() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'set_sortorder'));
+        $grade_item->set_sortorder(999);
+        $this->assertEquals($grade_item->sortorder, 999);
+    }
+
+    protected function sub_test_grade_item_move_after_sortorder() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'move_after_sortorder'));
+        $grade_item->move_after_sortorder(5);
+        $this->assertEquals($grade_item->sortorder, 6);
+
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[0]->id));
+        $this->assertEquals($grade_item->sortorder, 6);
+
+        $after = grade_item::fetch(array('id'=>$this->grade_items[6]->id));
+        $this->assertEquals($after->sortorder, 8);
+    }
+
+    protected function sub_test_grade_item_get_name() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_name'));
+
+        $name = $grade_item->get_name();
+        $this->assertEquals($this->grade_items[0]->itemname, $name);
+    }
+
+    protected function sub_test_grade_item_set_parent() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'set_parent'));
+
+        $old = $grade_item->get_parent_category();
+        $new = new grade_category($this->grade_categories[3], false);
+        $new_item = $new->get_grade_item();
+
+        $this->assertTrue($grade_item->set_parent($new->id));
+
+        $new_item->update_from_db();
+        $grade_item->update_from_db();
+
+        $this->assertEquals($grade_item->categoryid, $new->id);
+    }
+
+    protected function sub_test_grade_item_get_parent_category() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'get_parent_category'));
+
+        $category = $grade_item->get_parent_category();
+        $this->assertEquals($this->grade_categories[1]->fullname, $category->fullname);
+    }
+
+    protected function sub_test_grade_item_load_parent_category() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'load_parent_category'));
+
+        $category = $grade_item->load_parent_category();
+        $this->assertEquals($this->grade_categories[1]->fullname, $category->fullname);
+        $this->assertEquals($this->grade_categories[1]->fullname, $grade_item->parent_category->fullname);
+    }
+
+    protected function sub_test_grade_item_get_item_category() {
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $this->assertTrue(method_exists($grade_item, 'get_item_category'));
+
+        $category = $grade_item->get_item_category();
+        $this->assertEquals($this->grade_categories[0]->fullname, $category->fullname);
+    }
+
+    protected function sub_test_grade_item_load_item_category() {
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $this->assertTrue(method_exists($grade_item, 'load_item_category'));
+
+        $category = $grade_item->load_item_category();
+        $this->assertEquals($this->grade_categories[0]->fullname, $category->fullname);
+        $this->assertEquals($this->grade_categories[0]->fullname, $grade_item->item_category->fullname);
+    }
+
+    // Test update of all final grades
+    protected function sub_test_grade_item_regrade_final_grades() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'regrade_final_grades'));
+        $this->assertEquals(true, $grade_item->regrade_final_grades());
+        //TODO: add more tests
+    }
+
+    // Test the adjust_raw_grade method
+    protected function sub_test_grade_item_adjust_raw_grade() {
+        $grade_item = new grade_item($this->grade_items[2], false); // anything but assignment module!
+        $this->assertTrue(method_exists($grade_item, 'adjust_raw_grade'));
+
+        $grade_raw = new stdClass();
+        $grade_raw->rawgrade = 40;
+        $grade_raw->grademax = 100;
+        $grade_raw->grademin = 0;
+
+        $grade_item->gradetype = GRADE_TYPE_VALUE;
+        $grade_item->multfactor = 1;
+        $grade_item->plusfactor = 0;
+        $grade_item->grademax = 50;
+        $grade_item->grademin = 0;
+
+        $original_grade_raw  = clone($grade_raw);
+        $original_grade_item = clone($grade_item);
+
+        $this->assertEquals(20, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try a larger maximum grade
+        $grade_item->grademax = 150;
+        $grade_item->grademin = 0;
+        $this->assertEquals(60, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try larger minimum grade
+        $grade_item->grademin = 50;
+
+        $this->assertEquals(90, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Rescaling from a small scale (0-50) to a larger scale (0-100)
+        $grade_raw->grademax = 50;
+        $grade_raw->grademin = 0;
+        $grade_item->grademax = 100;
+        $grade_item->grademin = 0;
+
+        $this->assertEquals(80, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Rescaling from a small scale (0-50) to a larger scale with offset (40-100)
+        $grade_item->grademax = 100;
+        $grade_item->grademin = 40;
+
+        $this->assertEquals(88, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try multfactor and plusfactor
+        $grade_raw = clone($original_grade_raw);
+        $grade_item = clone($original_grade_item);
+        $grade_item->multfactor = 1.23;
+        $grade_item->plusfactor = 3;
+
+        $this->assertEquals(27.6, $grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax));
+
+        // Try multfactor below 0 and a negative plusfactor
+        $grade_raw = clone($original_grade_raw);
+        $grade_item = clone($original_grade_item);
+        $grade_item->multfactor = 0.23;
+        $grade_item->plusfactor = -3;
+
+        $this->assertEquals(round(1.6), round($grade_item->adjust_raw_grade($grade_raw->rawgrade, $grade_raw->grademin, $grade_raw->grademax)));
+    }
+
+    // Test locking of grade items
+    protected function sub_test_grade_item_set_locked() {
+        //getting a grade_item from the DB as set_locked() will fail if the grade items needs to be updated
+        //also needs to have at least one grade_grade or $grade_item->get_final(1) returns null
+        //$grade_item = new grade_item($this->grade_items[8]);
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[8]->id));
+
+        $this->assertTrue(method_exists($grade_item, 'set_locked'));
+
+        $grade_grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+        $this->assertTrue(empty($grade_item->locked));//not locked
+        $this->assertTrue(empty($grade_grade->locked));//not locked
+
+        $this->assertTrue($grade_item->set_locked(true, true, false));
+        $grade_grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+
+        $this->assertFalse(empty($grade_item->locked));//locked
+        $this->assertFalse(empty($grade_grade->locked)); // individual grades should be locked too
+
+        $this->assertTrue($grade_item->set_locked(false, true, false));
+        $grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+
+        $this->assertTrue(empty($grade_item->locked));
+        $this->assertTrue(empty($grade->locked)); // individual grades should be unlocked too
+    }
+
+    protected function sub_test_grade_item_is_locked() {
+        $grade_item = new grade_item($this->grade_items[10], false);
+        $this->assertTrue(method_exists($grade_item, 'is_locked'));
+
+        $this->assertFalse($grade_item->is_locked());
+        $this->assertFalse($grade_item->is_locked($this->user[1]->id));
+        $this->assertTrue($grade_item->set_locked(true, true, false));
+        $this->assertTrue($grade_item->is_locked());
+        $this->assertTrue($grade_item->is_locked($this->user[1]->id));
+    }
+
+    // Test hiding of grade items
+    protected function sub_test_grade_item_set_hidden() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'set_hidden'));
+
+        $grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+        $this->assertEquals(0, $grade_item->hidden);
+        $this->assertEquals(0, $grade->hidden);
+
+        $grade_item->set_hidden(666, true);
+        $grade = new grade_grade($grade_item->get_final($this->user[1]->id), false);
+
+        $this->assertEquals(666, $grade_item->hidden);
+        $this->assertEquals(666, $grade->hidden);
+    }
+
+    protected function sub_test_grade_item_is_hidden() {
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertTrue(method_exists($grade_item, 'is_hidden'));
+
+        $this->assertFalse($grade_item->is_hidden());
+        $this->assertFalse($grade_item->is_hidden(1));
+
+        $grade_item->set_hidden(1);
+        $this->assertTrue($grade_item->is_hidden());
+        $this->assertTrue($grade_item->is_hidden(1));
+
+        $grade_item->set_hidden(666);
+        $this->assertFalse($grade_item->is_hidden());
+        $this->assertFalse($grade_item->is_hidden(1));
+
+        $grade_item->set_hidden(time()+666);
+        $this->assertTrue($grade_item->is_hidden());
+        $this->assertTrue($grade_item->is_hidden(1));
+    }
+
+    protected function sub_test_grade_item_is_category_item() {
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $this->assertTrue(method_exists($grade_item, 'is_category_item'));
+        $this->assertTrue($grade_item->is_category_item());
+    }
+
+    protected function sub_test_grade_item_is_course_item() {
+        $grade_item = grade_item::fetch_course_item($this->courseid);
+        $this->assertTrue(method_exists($grade_item, 'is_course_item'));
+        $this->assertTrue($grade_item->is_course_item());
+    }
+
+    protected function sub_test_grade_item_fetch_course_item() {
+        $grade_item = grade_item::fetch_course_item($this->courseid);
+        $this->assertTrue(method_exists($grade_item, 'fetch_course_item'));
+        $this->assertEquals($grade_item->itemtype, 'course');
+    }
+
+    protected function sub_test_grade_item_depends_on() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+
+        // calculated grade dependency
+        $deps = $grade_item->depends_on();
+        sort($deps, SORT_NUMERIC); // for comparison
+        $this->assertEquals(array($this->grade_items[0]->id), $deps);
+
+        // simulate depends on returns none when locked
+        $grade_item->locked = time();
+        $grade_item->update();
+        $deps = $grade_item->depends_on();
+        sort($deps, SORT_NUMERIC); // for comparison
+        $this->assertEquals(array(), $deps);
+
+        // category dependency
+        $grade_item = new grade_item($this->grade_items[3], false);
+        $deps = $grade_item->depends_on();
+        sort($deps, SORT_NUMERIC); // for comparison
+        $res = array($this->grade_items[4]->id, $this->grade_items[5]->id);
+        $this->assertEquals($res, $deps);
+    }
+
+    protected function sub_test_grade_item_is_calculated() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+        $this->assertTrue(method_exists($grade_item, 'is_calculated'));
+        $this->assertTrue($grade_item->is_calculated());
+
+        $grade_item = new grade_item($this->grade_items[0], false);
+        $this->assertFalse($grade_item->is_calculated());
+    }
+
+    protected function sub_test_grade_item_set_calculation() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+        $this->assertTrue(method_exists($grade_item, 'set_calculation'));
+        $grade_itemsource = new grade_item($this->grade_items[0], false);
+
+        $grade_item->set_calculation('=[['.$grade_itemsource->idnumber.']]');
+
+        $this->assertTrue(!empty($grade_item->needsupdate));
+        $this->assertEquals('=##gi'.$grade_itemsource->id.'##', $grade_item->calculation);
+    }
+
+    protected function sub_test_grade_item_get_calculation() {
+        $grade_item = new grade_item($this->grade_items[1], false);
+        $this->assertTrue(method_exists($grade_item, 'get_calculation'));
+        $grade_itemsource = new grade_item($this->grade_items[0], false);
+
+        $denormalizedformula = str_replace('##gi'.$grade_itemsource->id.'##', '[['.$grade_itemsource->idnumber.']]', $this->grade_items[1]->calculation);
+
+        $formula = $grade_item->get_calculation();
+        $this->assertTrue(!empty($grade_item->needsupdate));
+        $this->assertEquals($denormalizedformula, $formula);
+    }
+
+    public function sub_test_grade_item_compute() {
+        $grade_item = grade_item::fetch(array('id'=>$this->grade_items[1]->id));
+        $this->assertTrue(method_exists($grade_item, 'compute'));
+
+        //check the grade_grades in the array match those in the DB then delete $this->grade_items[1]'s grade_grades
+        $this->grade_grades[3] = grade_grade::fetch(array('id'=>$this->grade_grades[3]->id));
+        $grade_grade = grade_grade::fetch(array('id'=>$this->grade_grades[3]->id));
+        $grade_grade->delete();
+
+        $this->grade_grades[4] = grade_grade::fetch(array('id'=>$this->grade_grades[4]->id));
+        $grade_grade = grade_grade::fetch(array('id'=>$this->grade_grades[4]->id));
+        $grade_grade->delete();
+
+        $this->grade_grades[5] = grade_grade::fetch(array('id'=>$this->grade_grades[5]->id));
+        $grade_grade = grade_grade::fetch(array('id'=>$this->grade_grades[5]->id));
+        $grade_grade->delete();
+
+        //recalculate the grades (its a calculation so pulls values from other grade_items) and reinsert them
+        $grade_item->compute();
+
+        $grade_grade = grade_grade::fetch(array('userid'=>$this->grade_grades[3]->userid, 'itemid'=>$this->grade_grades[3]->itemid));
+        $this->assertEquals($this->grade_grades[3]->finalgrade, $grade_grade->finalgrade);
+
+        $grade_grade = grade_grade::fetch(array('userid'=>$this->grade_grades[4]->userid, 'itemid'=>$this->grade_grades[4]->itemid));
+        $this->assertEquals($this->grade_grades[4]->finalgrade, $grade_grade->finalgrade);
+
+        $grade_grade = grade_grade::fetch(array('userid'=>$this->grade_grades[5]->userid, 'itemid'=>$this->grade_grades[5]->itemid));
+        $this->assertEquals($this->grade_grades[5]->finalgrade, $grade_grade->finalgrade);
+    }
+}
diff --git a/lib/grade/tests/grade_outcome_test.php b/lib/grade/tests/grade_outcome_test.php
new file mode 100644 (file)
index 0000000..78ea851
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_outcome_testcase extends grade_base_testcase {
+
+    public function test_grade_outcome() {
+        $this->sub_test_grade_outcome_construct();
+        $this->sub_test_grade_outcome_insert();
+        $this->sub_test_grade_outcome_update();
+        $this->sub_test_grade_outcome_delete();
+        //$this->sub_test_grade_outcome_fetch();
+        $this->sub_test_grade_outcome_fetch_all();
+    }
+
+    protected function sub_test_grade_outcome_construct() {
+        $params = new stdClass();
+
+        $params->courseid = $this->courseid;
+        $params->shortname = 'Team work';
+
+        $grade_outcome = new grade_outcome($params, false);
+        $this->assertEquals($params->courseid, $grade_outcome->courseid);
+        $this->assertEquals($params->shortname, $grade_outcome->shortname);
+    }
+
+    protected function sub_test_grade_outcome_insert() {
+        $grade_outcome = new grade_outcome();
+        $this->assertTrue(method_exists($grade_outcome, 'insert'));
+
+        $grade_outcome->courseid = $this->courseid;
+        $grade_outcome->shortname = 'tw';
+        $grade_outcome->fullname = 'Team work';
+
+        $grade_outcome->insert();
+
+        $last_grade_outcome = end($this->grade_outcomes);
+
+        $this->assertEquals($grade_outcome->id, $last_grade_outcome->id + 1);
+        $this->assertFalse(empty($grade_outcome->timecreated));
+        $this->assertFalse(empty($grade_outcome->timemodified));
+    }
+
+    protected function sub_test_grade_outcome_update() {
+        global $DB;
+        $grade_outcome = new grade_outcome($this->grade_outcomes[0], false);
+        $this->assertTrue(method_exists($grade_outcome, 'update'));
+        $grade_outcome->shortname = 'Team work';
+        $this->assertTrue($grade_outcome->update());
+        $shortname = $DB->get_field('grade_outcomes', 'shortname', array('id' => $this->grade_outcomes[0]->id));
+        $this->assertEquals($grade_outcome->shortname, $shortname);
+    }
+
+    protected function sub_test_grade_outcome_delete() {
+        global $DB;
+        $grade_outcome = new grade_outcome($this->grade_outcomes[0], false);
+        $this->assertTrue(method_exists($grade_outcome, 'delete'));
+
+        $this->assertTrue($grade_outcome->delete());
+        $this->assertFalse($DB->get_record('grade_outcomes', array('id' => $grade_outcome->id)));
+    }
+
+    protected function sub_test_grade_outcome_fetch() {
+        $grade_outcome = new grade_outcome();
+        $this->assertTrue(method_exists($grade_outcome, 'fetch'));
+
+        $grade_outcome = grade_outcome::fetch(array('id'=>$this->grade_outcomes[0]->id));
+        $grade_outcome->load_scale();
+        $this->assertEquals($this->grade_outcomes[0]->id, $grade_outcome->id);
+        $this->assertEquals($this->grade_outcomes[0]->shortname, $grade_outcome->shortname);
+
+        $this->assertEquals($this->scale[2]->id, $grade_outcome->scale->id);
+    }
+
+    protected function sub_test_grade_outcome_fetch_all() {
+        $grade_outcome = new grade_outcome();
+        $this->assertTrue(method_exists($grade_outcome, 'fetch_all'));
+
+        $grade_outcomes = grade_outcome::fetch_all(array());
+        $this->assertEquals(count($this->grade_outcomes), count($grade_outcomes));
+    }
+}
diff --git a/lib/grade/tests/grade_scale_test.php b/lib/grade/tests/grade_scale_test.php
new file mode 100644 (file)
index 0000000..f132222
--- /dev/null
@@ -0,0 +1,130 @@
+<?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/>.
+
+/**
+ * @package    core_grades
+ * @category   phpunit
+ * @copyright  nicolas@moodle.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/lib.php');
+
+
+class grade_scale_testcase extends grade_base_testcase {
+
+    public function test_grade_scale() {
+        $this->sub_test_scale_construct();
+        $this->sub_test_grade_scale_insert();
+        $this->sub_test_grade_scale_update();
+        $this->sub_test_grade_scale_delete();
+        $this->sub_test_grade_scale_fetch();
+        $this->sub_test_scale_load_items();
+        $this->sub_test_scale_compact_items();
+    }
+
+    protected function sub_test_scale_construct() {
+        $params = new stdClass();
+        $params->name        = 'unittestscale3';
+        $params->courseid    = $this->course->id;
+        $params->userid      = $this->userid;
+        $params->scale       = 'Distinction, Very Good, Good, Pass, Fail';
+        $params->description = 'This scale is used to mark standard assignments.';
+        $params->timemodified = time();
+
+        $scale = new grade_scale($params, false);
+
+        $this->assertEquals($params->name, $scale->name);
+        $this->assertEquals($params->scale, $scale->scale);
+        $this->assertEquals($params->description, $scale->description);
+
+    }
+
+    protected function sub_test_grade_scale_insert() {
+        $grade_scale = new grade_scale();
+        $this->assertTrue(method_exists($grade_scale, 'insert'));
+
+        $grade_scale->name        = 'unittestscale3';
+        $grade_scale->courseid    = $this->courseid;
+        $grade_scale->userid      = $this->userid;
+        $grade_scale->scale       = 'Distinction, Very Good, Good, Pass, Fail';
+        $grade_scale->description = 'This scale is used to mark standard assignments.';
+
+        $grade_scale->insert();
+
+        $last_grade_scale = end($this->scale);
+
+        $this->assertEquals($grade_scale->id, $last_grade_scale->id + 1);
+        $this->assertTrue(!empty($grade_scale->timecreated));
+        $this->assertTrue(!empty($grade_scale->timemodified));
+    }
+
+    protected function sub_test_grade_scale_update() {
+        global $DB;
+        $grade_scale = new grade_scale($this->scale[1], false);
+        $this->assertTrue(method_exists($grade_scale, 'update'));
+
+        $grade_scale->name = 'Updated info for this unittest grade_scale';
+        $this->assertTrue($grade_scale->update());
+        $name = $DB->get_field('scale', 'name', array('id' => $this->scale[1]->id));
+        $this->assertEquals($grade_scale->name, $name);
+    }
+
+    protected function sub_test_grade_scale_delete() {
+        global $DB;
+        $grade_scale = new grade_scale($this->scale[4], false);//choose one we're not using elsewhere
+        $this->assertTrue(method_exists($grade_scale, 'delete'));
+
+        $this->assertTrue($grade_scale->delete());
+        $this->assertFalse($DB->get_record('scale', array('id' => $grade_scale->id)));
+
+        //keep the reference collection the same as what is in the database
+        unset($this->scale[4]);
+    }
+
+    protected function sub_test_grade_scale_fetch() {
+        $grade_scale = new grade_scale();
+        $this->assertTrue(method_exists($grade_scale, 'fetch'));
+
+        $grade_scale = grade_scale::fetch(array('id'=>$this->scale[0]->id));
+        $this->assertEquals($this->scale[0]->id, $grade_scale->id);
+        $this->assertEquals($this->scale[0]->name, $grade_scale->name);
+    }
+
+    protected function sub_test_scale_load_items() {
+        $scale = new grade_scale($this->scale[0], false);
+        $this->assertTrue(method_exists($scale, 'load_items'));
+
+        $scale->load_items();
+        $this->assertEquals(7, count($scale->scale_items));
+        $this->assertEquals('Fairly neutral', $scale->scale_items[2]);
+
+    }
+
+    protected function sub_test_scale_compact_items() {
+        $scale = new grade_scale($this->scale[0], false);
+        $this->assertTrue(method_exists($scale, 'compact_items'));
+
+        $scale->load_items();
+        $scale->scale = null;
+        $scale->compact_items();
+
+        // The original string and the new string may have differences in whitespace around the delimiter, and that's OK
+        $this->assertEquals(preg_replace('/\s*,\s*/', ',', $this->scale[0]->scale), $scale->scale);
+    }
+}
index 60def99..43c3e57 100644 (file)
@@ -8734,47 +8734,6 @@ function moodle_major_version($fromdisk = false) {
     }
 }
 
-/**
- * Sets maximum expected time needed for upgrade task.
- * Please always make sure that upgrade will not run longer!
- *
- * The script may be automatically aborted if upgrade times out.
- *
- * @category upgrade
- * @param int $max_execution_time in seconds (can not be less than 60 s)
- * @todo MDL-32293 - Move this function to lib/upgradelib.php
- */
-function upgrade_set_timeout($max_execution_time=300) {
-    global $CFG;
-
-    if (!isset($CFG->upgraderunning) or $CFG->upgraderunning < time()) {
-        $upgraderunning = get_config(null, 'upgraderunning');
-    } else {
-        $upgraderunning = $CFG->upgraderunning;
-    }
-
-    if (!$upgraderunning) {
-        // upgrade not running or aborted
-        print_error('upgradetimedout', 'admin', "$CFG->wwwroot/$CFG->admin/");
-        die;
-    }
-
-    if ($max_execution_time < 60) {
-        // protection against 0 here
-        $max_execution_time = 60;
-    }
-
-    $expected_end = time() + $max_execution_time;
-
-    if ($expected_end < $upgraderunning + 10 and $expected_end > $upgraderunning - 10) {
-        // no need to store new end, it is nearly the same ;-)
-        return;
-    }
-
-    set_time_limit($max_execution_time);
-    set_config('upgraderunning', $expected_end); // keep upgrade locked until this time
-}
-
 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
 
 /**
index 204eb9b..a2974c9 100644 (file)
@@ -1038,7 +1038,7 @@ class global_navigation extends navigation_node {
         } else {
             // The home element should be the site because the root node is my moodle
             $this->rootnodes['home'] = $this->add(get_string('sitehome'), new moodle_url('/'), self::TYPE_SETTING, null, 'home');
-            if ($CFG->defaulthomepage == HOMEPAGE_MY) {
+            if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY)) {
                 // We need to stop automatic redirection
                 $this->rootnodes['home']->action->param('redirect', '0');
             }
index a862c42..280e19f 100644 (file)
@@ -941,6 +941,9 @@ class theme_config {
                     return $imagefile;
                 }
             }
+            if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image")) {
+                return $imagefile;
+            }
             if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image")) {
                 return $imagefile;
             }
@@ -974,6 +977,9 @@ class theme_config {
                     return $imagefile;
                 }
             }
+            if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image")) {
+                return $imagefile;
+            }
             $dir = get_plugin_directory($type, $plugin);
             if ($imagefile = $this->image_exists("$dir/pix/$image")) {
                 return $imagefile;
index 169d16f..3b72e6e 100644 (file)
@@ -67,7 +67,7 @@ define('K_PATH_URL', $CFG->wwwroot . '/lib/tcpdf/');
 define('K_PATH_FONTS', K_PATH_MAIN . 'fonts/');
 
 /** cache directory for temporary files (full path) */
-define('K_PATH_CACHE', $CFG->cachedir . '/');
+define('K_PATH_CACHE', $CFG->cachedir . '/tcpdf/');
 
 /** images directory */
 define('K_PATH_IMAGES', $CFG->dirroot . '/');
@@ -99,6 +99,7 @@ class pdf extends TCPDF {
      * See the parent class documentation for the parameters info.
      */
     public function __construct($orientation='P', $unit='mm', $format='A4', $unicode=true, $encoding='UTF-8') {
+        make_cache_directory('tcpdf');
 
         parent::__construct($orientation, $unit, $format, $unicode, $encoding);
 
index 7d7e3e2..f6ac22f 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Prepares PHPUnit environment, it is called automatically.
+ * Prepares PHPUnit environment, the phpunit.xml configuration
+ * must specify this file as bootstrap.
  *
  * Exit codes:
  *  0   - success
  *  1   - general error
- *  130 - coding error
+ *  130 - missing PHPUnit library error
  *  131 - configuration problem
  *  132 - install new test database
- *  133 - drop old data, then install new test database
+ *  133 - drop existing data before installing
  *
  * @package    core
  * @category   phpunit
@@ -36,29 +37,39 @@ error_reporting(E_ALL | E_STRICT);
 ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
+require_once(__DIR__.'/bootstraplib.php');
+
 if (isset($_SERVER['REMOTE_ADDR'])) {
-    phpunit_bootstrap_error('Unit tests can be executed only from command line!', 1);
+    phpunit_bootstrap_error(1, 'Unit tests can be executed only from command line!');
 }
 
 if (defined('PHPUNIT_TEST')) {
-    phpunit_bootstrap_error("PHPUNIT_TEST constant must not be manually defined anywhere!", 130);
+    phpunit_bootstrap_error(1, "PHPUNIT_TEST constant must not be manually defined anywhere!");
 }
 /** PHPUnit testing framework active */
 define('PHPUNIT_TEST', true);
 
 if (!defined('PHPUNIT_UTIL')) {
-    /** Identifies utility scripts */
+    /** Identifies utility scripts - the database does not need to be initialised */
     define('PHPUNIT_UTIL', false);
 }
 
 if (defined('CLI_SCRIPT')) {
-    phpunit_bootstrap_error('CLI_SCRIPT must not be manually defined in any PHPUnit test scripts', 130);
+    phpunit_bootstrap_error(1, 'CLI_SCRIPT must not be manually defined in any PHPUnit test scripts');
 }
 define('CLI_SCRIPT', true);
 
+$phpunitversion = PHPUnit_Runner_Version::id();
+if ($phpunitversion === '@package_version@') {
+    // library checked out from git, let's hope dev knows that 3.6.0 is required
+} else if (version_compare($phpunitversion, '3.6.0', 'lt')) {
+    phpunit_bootstrap_error(129, $phpunitversion);
+}
+unset($phpunitversion);
+
 define('NO_OUTPUT_BUFFERING', true);
 
-// only load CFG from config.php
+// only load CFG from config.php, stop ASAP in lib/setup.php
 define('ABORT_AFTER_CONFIG', true);
 require(__DIR__ . '/../../config.php');
 
@@ -82,37 +93,38 @@ if (isset($CFG->phpunit_directorypermissions)) {
 }
 $CFG->filepermissions = ($CFG->directorypermissions & 0666);
 if (!isset($CFG->phpunit_dataroot)) {
-    phpunit_bootstrap_error('Missing $CFG->phpunit_dataroot in config.php, can not run tests!', 131);
+    phpunit_bootstrap_error(131, 'Missing $CFG->phpunit_dataroot in config.php, can not run tests!');
 }
 if (isset($CFG->dataroot) and $CFG->phpunit_dataroot === $CFG->dataroot) {
-    phpunit_bootstrap_error('$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!');
 }
 if (!file_exists($CFG->phpunit_dataroot)) {
     mkdir($CFG->phpunit_dataroot, $CFG->directorypermissions);
 }
 if (!is_dir($CFG->phpunit_dataroot)) {
-    phpunit_bootstrap_error('$CFG->phpunit_dataroot directory can not be created, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory can not be created, can not run tests!');
 }
+
 if (!is_writable($CFG->phpunit_dataroot)) {
-    // try to fix premissions if possible
+    // try to fix permissions if possible
     if (function_exists('posix_getuid')) {
         $chmod = fileperms($CFG->phpunit_dataroot);
-        if (fileowner($dir) == posix_getuid()) {
+        if (fileowner($CFG->phpunit_dataroot) == posix_getuid()) {
             $chmod = $chmod | 0700;
             chmod($CFG->phpunit_dataroot, $chmod);
         }
     }
     if (!is_writable($CFG->phpunit_dataroot)) {
-        phpunit_bootstrap_error('$CFG->phpunit_dataroot directory is not writable, can not run tests!', 131);
+        phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory is not writable, can not run tests!');
     }
 }
 if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
     if ($dh = opendir($CFG->phpunit_dataroot)) {
         while (($file = readdir($dh)) !== false) {
-            if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_store') {
+            if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_Store') {
                 continue;
             }
-            phpunit_bootstrap_error('$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?', 131);
+            phpunit_bootstrap_error(131, '$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?');
         }
         closedir($dh);
         unset($dh);
@@ -123,22 +135,28 @@ if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
     phpunit_bootstrap_initdataroot($CFG->phpunit_dataroot);
 }
 
-
 // verify db prefix
 if (!isset($CFG->phpunit_prefix)) {
-    phpunit_bootstrap_error('Missing $CFG->phpunit_prefix in config.php, can not run tests!', 131);
+    phpunit_bootstrap_error(131, 'Missing $CFG->phpunit_prefix in config.php, can not run tests!');
 }
 if ($CFG->phpunit_prefix === '') {
-    phpunit_bootstrap_error('$CFG->phpunit_prefix can not be empty, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->phpunit_prefix can not be empty, can not run tests!');
 }
 if (isset($CFG->prefix) and $CFG->prefix === $CFG->phpunit_prefix) {
-    phpunit_bootstrap_error('$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!', 131);
+    phpunit_bootstrap_error(131, '$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!');
 }
 
-// throw away standard CFG settings
-
-$CFG->dataroot = $CFG->phpunit_dataroot;
-$CFG->prefix = $CFG->phpunit_prefix;
+// override CFG settings if necessary and throw away extra CFG settings
+$CFG->dataroot  = $CFG->phpunit_dataroot;
+$CFG->prefix    = $CFG->phpunit_prefix;
+$CFG->dbtype    = isset($CFG->phpunit_dbtype) ? $CFG->phpunit_dbtype : $CFG->dbtype;
+$CFG->dblibrary = isset($CFG->phpunit_dblibrary) ? $CFG->phpunit_dblibrary : $CFG->dblibrary;
+$CFG->dbhost    = isset($CFG->phpunit_dbhost) ? $CFG->phpunit_dbhost : $CFG->dbhost;
+$CFG->dbname    = isset($CFG->phpunit_dbname) ? $CFG->phpunit_dbname : $CFG->dbname;
+$CFG->dbuser    = isset($CFG->phpunit_dbuser) ? $CFG->phpunit_dbuser : $CFG->dbuser;
+$CFG->dbpass    = isset($CFG->phpunit_dbpass) ? $CFG->phpunit_dbpass : $CFG->dbpass;
+$CFG->prefix    = isset($CFG->phpunit_prefix) ? $CFG->phpunit_prefix : $CFG->prefix;
+$CFG->dboptions = isset($CFG->phpunit_dboptions) ? $CFG->phpunit_dboptions : $CFG->dboptions;
 
 $allowed = array('wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
                  'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions');
@@ -161,7 +179,7 @@ $CFG->debug = (E_ALL | E_STRICT); // can not use DEBUG_DEVELOPER yet
 $CFG->debugdisplay = 1;
 error_reporting($CFG->debug);
 ini_set('display_errors', '1');
-ini_set('log_errors', '0');
+ini_set('log_errors', '1');
 
 $CFG->passwordsaltmain = 'phpunit'; // makes login via normal UI impossible
 
@@ -182,54 +200,15 @@ require("$CFG->dirroot/lib/setup.php");
 raise_memory_limit(MEMORY_EXTRA);
 
 if (PHPUNIT_UTIL) {
-    // we are not going to do testing, this is 'true' in utility scripts that init database usually
+    // we are not going to do testing, this is 'true' in utility scripts that only init database
     return;
 }
 
 // is database and dataroot ready for testing?
-$problem = phpunit_util::testing_ready_problem();
-
-if ($problem) {
-    switch ($problem) {
-        case 132:
-            phpunit_bootstrap_error('Database was not initialised to run unit tests, please use "php admin/tool/phpunit/cli/util.php --install"', $problem);
-        case 133:
-            phpunit_bootstrap_error('Database was initialised for different version, please use "php admin/tool/phpunit/cli/util.php --drop; php admin/tool/phpunit/cli/util.php --install"', $problem);
-        default:
-            phpunit_bootstrap_error('Unknown problem initialising test database', $problem);
-    }
+list($errorcode, $message) = phpunit_util::testing_ready_problem();
+if ($errorcode) {
+    phpunit_bootstrap_error($errorcode, $message);
 }
 
-// prepare for the first test run - store fresh globals, reset dataroot, etc.
+// prepare for the first test run - store fresh globals, reset database and dataroot, etc.
 phpunit_util::bootstrap_init();
-
-
-//=========================================================
-
-/**
- * Print error and stop execution
- * @param string $text An error message to display
- * @param int $errorcode The error code (see docblock for detailed list)
- * @return void stops code execution with error code
- */
-function phpunit_bootstrap_error($text, $errorcode = 1) {
-    fwrite(STDERR, $text."\n");
-    exit($errorcode);
-}
-
-/**
- * Mark empty dataroot to be used for testing.
- * @param string $dataroot The dataroot directory
- * @return void
- */
-function phpunit_bootstrap_initdataroot($dataroot) {
-    global $CFG;
-
-    if (!file_exists("$dataroot/phpunittestdir.txt")) {
-        file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!');
-    }
-    chmod("$dataroot/phpunittestdir.txt", $CFG->filepermissions);
-    if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
-        mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions);
-    }
-}
diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php
new file mode 100644 (file)
index 0000000..d87c23f
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * PHPUnit bootstrap function
+ *
+ * Note: these functions must be self contained and must not rely on any library or include
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Print error and stop execution
+ * @param int $errorcode The exit error code
+ * @param string $text An error message to display
+ * @return void stops code execution with error code
+ */
+function phpunit_bootstrap_error($errorcode, $text = '') {
+    switch ($errorcode) {
+        case 0:
+            // this is not an error, just print information and exit
+            break;
+        case 1:
+            $text = 'Error: '.$text;
+            break;
+        case 129:
+            $text = 'Moodle requires PHPUnit 3.6.x, '.$text.' is not compatible';
+            break;
+        case 130:
+            $text = 'Moodle can not find PHPUnit PEAR library or necessary PHPUnit extension';
+            break;
+        case 131:
+            $text = 'Moodle configuration problem: '.$text;
+            break;
+        case 132:
+            $text = "Moodle PHPUnit environment is not initialised, please use:\n php admin/tool/phpunit/cli/util.php --install";
+            break;
+        case 133:
+            $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php admin/tool/phpunit/cli/util.php --drop\n php admin/tool/phpunit/cli/util.php --install";
+            break;
+        case 134:
+            $text = 'Moodle can not create PHPUnit configuration file, please verify dirroot permissions';
+            break;
+        default:
+            $text = empty($text) ? '' : ': '.$text;
+            $text = 'Unknown error '.$errorcode.$text;
+            break;
+    }
+    if (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL) {
+        // do not write to error stream because we need the error message in PHP exec result from web ui
+        echo($text."\n");
+    } else {
+        fwrite(STDERR, $text."\n");
+    }
+    exit($errorcode);
+}
+
+/**
+ * Mark empty dataroot to be used for testing.
+ * @param string $dataroot The dataroot directory
+ * @return void
+ */
+function phpunit_bootstrap_initdataroot($dataroot) {
+    global $CFG;
+    umask(0);
+    if (!file_exists("$dataroot/phpunittestdir.txt")) {
+        file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!');
+    }
+    phpunit_boostrap_fix_file_permissions("$dataroot/phpunittestdir.txt");
+    if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
+        mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions);
+    }
+}
+
+/**
+ * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
+ * @param string $file
+ * @return bool success
+ */
+function phpunit_boostrap_fix_file_permissions($file) {
+    global $CFG;
+
+    $permissions = fileperms($file);
+    if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
+        $permissions = $permissions | $CFG->filepermissions;
+        return chmod($file, $permissions);
+    }
+
+    return true;
+}
index f01893e..18766fe 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+
 /**
  * Data generator for unit tests
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class phpunit_data_generator {
     protected $usercounter = 0;
     protected $categorycount = 0;
     protected $coursecount = 0;
-    protected $blockcount = 0;
-    protected $modulecount = 0;
     protected $scalecount = 0;
+    protected $groupcount = 0;
+    protected $groupingcount = 0;
+
+    /** @var array list of plugin generators */
+    protected $generators = array();
+
+    /** @var array lis of common last names */
+    public $lastnames = array(
+        'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson',
+        'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann',
+        'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová',
+        'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова',
+        '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周',
+        '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤',
+    );
+
+    /** @var array lis of common first names */
+    public $firstnames = array(
+        'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava',
+        'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura',
+        'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína',
+        'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина',
+        '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏',
+        '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽',
+    );
+
+    public $loremipsum = <<<EOD
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla non arcu lacinia neque faucibus fringilla. Vivamus porttitor turpis ac leo. Integer in sapien. Nullam eget nisl. Aliquam erat volutpat. Cras elementum. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Integer malesuada. Nullam lectus justo, vulputate eget mollis sed, tempor sed magna. Mauris elementum mauris vitae tortor. Aliquam erat volutpat.
+Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Pellentesque ipsum. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Aliquam ante. Proin in tellus sit amet nibh dignissim sagittis. Vivamus porttitor turpis ac leo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Aliquam erat volutpat. Nulla est.
+Vivamus luctus egestas leo. Aenean fermentum risus id tortor. Mauris dictum facilisis augue. Aliquam erat volutpat. Aliquam ornare wisi eu metus. Aliquam id dolor. Duis condimentum augue id magna semper rutrum. Donec iaculis gravida nulla. Pellentesque ipsum. Etiam dictum tincidunt diam. Quisque tincidunt scelerisque libero. Etiam egestas wisi a erat.
+Integer lacinia. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris tincidunt sem sed arcu. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam id dolor. Maecenas sollicitudin. Et harum quidem rerum facilis est et expedita distinctio. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Nullam dapibus fermentum ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Pellentesque sapien. Duis risus. Mauris elementum mauris vitae tortor. Suspendisse nisl. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim.
+In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Maecenas lorem. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Curabitur ligula sapien, pulvinar a vestibulum quis, facilisis vel sapien. Nam sed tellus id magna elementum tincidunt. Suspendisse nisl. Vivamus luctus egestas leo. Nulla non arcu lacinia neque faucibus fringilla. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Etiam dictum tincidunt diam. Etiam commodo dui eget wisi. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Duis ante orci, molestie vitae vehicula venenatis, tincidunt ac pede. Pellentesque sapien.
+EOD;
 
     /**
      * To be called from data reset code only,
@@ -45,9 +82,37 @@ class phpunit_data_generator {
         $this->usercounter = 0;
         $this->categorycount = 0;
         $this->coursecount = 0;
-        $this->blockcount = 0;
-        $this->modulecount = 0;
         $this->scalecount = 0;
+
+        foreach($this->generators as $generator) {
+            $generator->reset();
+        }
+    }
+
+    /**
+     * Return generator for given plugin
+     * @param string $component
+     * @return mixed plugin data generator
+     */
+    public function get_plugin_generator($component) {
+        list($type, $plugin) = normalize_component($component);
+
+        if ($type !== 'mod' and $type !== 'block') {
+            throw new coding_exception("Plugin type $type does not support generators yet");
+        }
+
+        $dir = get_plugin_directory($type, $plugin);
+
+        if (!isset($this->generators[$type.'_'.$plugin])) {
+            $lib = "$dir/tests/generator/lib.php";
+            if (!include_once($lib)) {
+                throw new coding_exception("Plugin $component does not support data generator, missing tests/generator/lib");
+            }
+            $classname = $type.'_'.$plugin.'_generator';
+            $this->generators[$type.'_'.$plugin] = new $classname($this);
+        }
+
+        return $this->generators[$type.'_'.$plugin];
     }
 
     /**
@@ -68,11 +133,18 @@ class phpunit_data_generator {
             $record['auth'] = 'manual';
         }
 
-        if (!isset($record['firstname'])) {
+        if (!isset($record['firstname']) and !isset($record['lastname'])) {
+            $country = rand(0, 5);
+            $firstname = rand(0, 4);
+            $lastname = rand(0, 4);
+            $female = rand(0, 1);
+            $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
+            $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
+
+        } else if (!isset($record['firstname'])) {
             $record['firstname'] = 'Firstname'.$i;
-        }
 
-        if (!isset($record['lastname'])) {
+        } else if (!isset($record['lastname'])) {
             $record['lastname'] = 'Lastname'.$i;
         }
 
@@ -80,8 +152,15 @@ class phpunit_data_generator {
             $record['idnumber'] = '';
         }
 
+        if (!isset($record['mnethostid'])) {
+            $record['mnethostid'] = $CFG->mnet_localhost_id;
+        }
+
         if (!isset($record['username'])) {
-            $record['username'] = 'username'.$i;
+            $record['username'] = textlib::strtolower($record['firstname']).textlib::strtolower($record['lastname']);
+            while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
+                $record['username'] = $record['username'].'_'.$i;
+            }
         }
 
         if (!isset($record['password'])) {
@@ -96,10 +175,6 @@ class phpunit_data_generator {
             $record['confirmed'] = 1;
         }
 
-        if (!isset($record['mnethostid'])) {
-            $record['mnethostid'] = $CFG->mnet_localhost_id;
-        }
-
         if (!isset($record['lang'])) {
             $record['lang'] = 'en';
         }
@@ -129,6 +204,7 @@ class phpunit_data_generator {
         }
 
         $userid = $DB->insert_record('user', $record);
+
         if (!$record['deleted']) {
             context_user::instance($userid);
         }
@@ -160,7 +236,7 @@ class phpunit_data_generator {
         }
 
         if (!isset($record['description'])) {
-            $record['description'] = 'Test course category '.$i;
+            $record['description'] = "Test course category $i\n$this->loremipsum";
         }
 
         if (!isset($record['descriptionformat'])) {
@@ -171,7 +247,7 @@ class phpunit_data_generator {
             $record['descriptionformat'] = 0;
         }
 
-        if ($record['parent'] == 0) {
+        if (empty($record['parent'])) {
             $parent = new stdClass();
             $parent->path = '';
             $parent->depth = 0;
@@ -235,7 +311,7 @@ class phpunit_data_generator {
         }
 
         if (!isset($record['description'])) {
-            $record['description'] = 'Test course '.$i;
+            $record['description'] = "Test course $i\n$this->loremipsum";
         }
 
         if (!isset($record['descriptionformat'])) {
@@ -313,108 +389,106 @@ class phpunit_data_generator {
      * @return stdClass block instance record
      */
     public function create_block($blockname, $record=null, array $options=null) {
-        global $DB;
+        $generator = $this->get_plugin_generator('block_'.$blockname);
+        return $generator->create_instance($record, $options);
+    }
 
-        $this->blockcount++;
-        $i = $this->blockcount;
+    /**
+     * Create a test module
+     * @param string $modulename
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record
+     */
+    public function create_module($modulename, $record=null, array $options=null) {
+        $generator = $this->get_plugin_generator('mod_'.$modulename);
+        return $generator->create_instance($record, $options);
+    }
 
-        $record = (array)$record;
+    /**
+     * Create a test group for the specified course
+     *
+     * $record should be either an array or a stdClass containing infomation about the group to create.
+     * At the very least it needs to contain courseid.
+     * Default values are added for name, description, and descriptionformat if they are not present.
+     *
+     * This function calls {@see groups_create_group()} to create the group within the database.
+     *
+     * @param array|stdClass $record
+     * @return stdClass group record
+     */
+    public function create_group($record) {
+        global $DB, $CFG;
 
-        $record['blockname'] = $blockname;
+        require_once($CFG->dirroot . '/group/lib.php');
 
-        //TODO: use block callbacks
+        $this->groupcount++;
+        $i = $this->groupcount;
 
-        if (!isset($record['parentcontextid'])) {
-            $record['parentcontextid'] = context_system::instance()->id;
-        }
+        $record = (array)$record;
 
-        if (!isset($record['showinsubcontexts'])) {
-            $record['showinsubcontexts'] = 1;
+        if (empty($record['courseid'])) {
+            throw new coding_exception('courseid must be present in phpunit_util::create_group() $record');
         }
 
-        if (!isset($record['pagetypepattern'])) {
-            $record['pagetypepattern'] = '';
+        if (!isset($record['name'])) {
+            $record['name'] = 'group-' . $i;
         }
 
-        if (!isset($record['subpagepattern'])) {
-            $record['subpagepattern'] = '';
+        if (!isset($record['description'])) {
+            $record['description'] = "Test Group $i\n{$this->loremipsum}";
         }
 
-        if (!isset($record['defaultweight'])) {
-            $record['defaultweight'] = '';
+        if (!isset($record['descriptionformat'])) {
+            $record['descriptionformat'] = FORMAT_MOODLE;
         }
 
-        $biid = $DB->insert_record('block_instances', $record);
-        context_block::instance($biid);
+        $id = groups_create_group((object)$record);
 
-        return $DB->get_record('block_instances', array('id'=>$biid), '*', MUST_EXIST);
+        return $DB->get_record('groups', array('id'=>$id));
     }
 
     /**
-     * Create a test module
-     * @param string $modulename
+     * Create a test grouping for the specified course
+     *
+     * $record should be either an array or a stdClass containing infomation about the grouping to create.
+     * At the very least it needs to contain courseid.
+     * Default values are added for name, description, and descriptionformat if they are not present.
+     *
+     * This function calls {@see groups_create_grouping()} to create the grouping within the database.
+     *
      * @param array|stdClass $record
-     * @param array $options
-     * @return stdClass activity record
+     * @return stdClass grouping record
      */
-    public function create_module($modulename, $record=null, array $options=null) {
+    public function create_grouping($record) {
         global $DB, $CFG;
-        require_once("$CFG->dirroot/course/lib.php");
 
-        $this->modulecount++;
-        $i = $this->modulecount;
+        require_once($CFG->dirroot . '/group/lib.php');
 
-        $record = (array)$record;
-        $options = (array)$options;
+        $this->groupingcount++;
+        $i = $this->groupingcount;
 
-        if (!isset($record['name'])) {
-            $record['name'] = get_string('pluginname', $modulename).' '.$i;
-        }
+        $record = (array)$record;
 
-        if (!isset($record['intro'])) {
-            $record['intro'] = 'Test module '.$i;
+        if (empty($record['courseid'])) {
+            throw new coding_exception('courseid must be present in phpunit_util::create_grouping() $record');
         }
 
-        if (!isset($record['introformat'])) {
-            $record['introformat'] = FORMAT_MOODLE;
+        if (!isset($record['name'])) {
+            $record['name'] = 'grouping-' . $i;
         }
 
-        if (!isset($options['section'])) {
-            $options['section'] = 1;
+        if (!isset($record['description'])) {
+            $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
         }
 
-        //TODO: use module callbacks
-
-        if ($modulename === 'page') {
-            if (!isset($record['content'])) {
-                $record['content'] = 'Test page content';
-            }
-            if (!isset($record['contentformat'])) {
-                $record['contentformat'] = FORMAT_MOODLE;
-            }
-
-        } else {
-            error('TODO: only mod_page is supported in data generator for now');
+        if (!isset($record['descriptionformat'])) {
+            $record['descriptionformat'] = FORMAT_MOODLE;
         }
 
-        $id = $DB->insert_record($modulename, $record);
-
-        $cm = new stdClass();
-        $cm->course   = $record['course'];
-        $cm->module   = $DB->get_field('modules', 'id', array('name'=>$modulename));
-        $cm->section  = $options['section'];
-        $cm->instance = $id;
-        $cm->id = $DB->insert_record('course_modules', $cm);
-
-        $cm->coursemodule = $cm->id;
-        add_mod_to_section($cm);
-
-        context_module::instance($cm->id);
-
-        $instance = $DB->get_record($modulename, array('id'=>$id), '*', MUST_EXIST);
-        $instance->cmid = $cm->id;
+        $id = groups_create_grouping((object)$record);
 
-        return $instance;
+        return $DB->get_record('groupings', array('id'=>$id));
     }
 
     /**
@@ -468,3 +542,191 @@ class phpunit_data_generator {
         return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
     }
 }
+
+
+/**
+ * Module generator base class.
+ *
+ * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class phpunit_module_generator {
+    /** @var phpunit_data_generator@var  */
+    protected $datagenerator;
+
+    /** @var number of created instances */
+    protected $instancecount = 0;
+
+    public function __construct(phpunit_data_generator $datagenerator) {
+        $this->datagenerator = $datagenerator;
+    }
+
+    /**
+     * To be called from data reset code only,
+     * do not use in tests.
+     * @return void
+     */
+    public function reset() {
+        $this->instancecount = 0;
+    }
+
+    /**
+     * Returns module name
+     * @return string name of module that this class describes
+     * @throws coding_exception if class invalid
+     */
+    public function get_modulename() {
+        $matches = null;
+        if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) {
+            throw new coding_exception('Invalid module generator class name: '.get_class($this));
+        }
+
+        if (empty($matches[1])) {
+            throw new coding_exception('Invalid module generator class name: '.get_class($this));
+        }
+        return $matches[1];
+    }
+
+    /**
+     * Create course module and link it to course
+     * @param stdClass $instance
+     * @param array $options: section, visible
+     * @return stdClass $cm instance
+     */
+    protected function create_course_module(stdClass $instance, array $options) {
+        global $DB, $CFG;
+        require_once("$CFG->dirroot/course/lib.php");
+
+        $modulename = $this->get_modulename();
+
+        $cm = new stdClass();
+        $cm->course             = $instance->course;
+        $cm->module             = $DB->get_field('modules', 'id', array('name'=>$modulename));
+        $cm->instance           = $instance->id;
+        $cm->section            = isset($options['section']) ? $options['section'] : 0;
+        $cm->idnumber           = isset($options['idnumber']) ? $options['idnumber'] : 0;
+        $cm->added              = time();
+
+        $columns = $DB->get_columns('course_modules');
+        foreach ($options as $key=>$value) {
+            if ($key === 'id' or !isset($columns[$key])) {
+                continue;
+            }
+            if (property_exists($cm, $key)) {
+                continue;
+            }
+            $cm->$key = $value;
+        }
+
+        $cm->id = $DB->insert_record('course_modules', $cm);
+        $cm->coursemodule = $cm->id;
+
+        add_mod_to_section($cm);
+
+        $cm = get_coursemodule_from_id($modulename, $cm->id, $cm->course, true, MUST_EXIST);
+
+        context_module::instance($cm->id);
+
+        return $cm;
+    }
+
+    /**
+     * Create a test module
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record
+     */
+    abstract public function create_instance($record = null, array $options = null);
+}
+
+
+/**
+ * Block generator base class.
+ *
+ * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class phpunit_block_generator {
+    /** @var phpunit_data_generator@var  */
+    protected $datagenerator;
+
+    /** @var number of created instances */
+    protected $instancecount = 0;
+
+    public function __construct(phpunit_data_generator $datagenerator) {
+        $this->datagenerator = $datagenerator;
+    }
+
+    /**
+     * To be called from data reset code only,
+     * do not use in tests.
+     * @return void
+     */
+    public function reset() {
+        $this->instancecount = 0;
+    }
+
+    /**
+     * Returns block name
+     * @return string name of block that this class describes
+     * @throws coding_exception if class invalid
+     */
+    public function get_blockname() {
+        $matches = null;
+        if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) {
+            throw new coding_exception('Invalid block generator class name: '.get_class($this));
+        }
+
+        if (empty($matches[1])) {
+            throw new coding_exception('Invalid block generator class name: '.get_class($this));
+        }
+        return $matches[1];
+    }
+
+    /**
+     * Fill in record defaults
+     * @param stdClass $record
+     * @return stdClass
+     */
+    protected function prepare_record(stdClass $record) {
+        $record->blockname = $this->get_blockname();
+        if (!isset($record->parentcontextid)) {
+            $record->parentcontextid = context_system::instance()->id;
+        }
+        if (!isset($record->showinsubcontexts)) {
+            $record->showinsubcontexts = 1;
+        }
+        if (!isset($record->pagetypepattern)) {
+            $record->pagetypepattern = '';
+        }
+        if (!isset($record->subpagepattern)) {
+            $record->subpagepattern = null;
+        }
+        if (!isset($record->defaultregion)) {
+            $record->defaultregion = '';
+        }
+        if (!isset($record->defaultweight)) {
+            $record->defaultweight = '';
+        }
+        if (!isset($record->configdata)) {
+            $record->configdata = null;
+        }
+        return $record;
+    }
+
+    /**
+     * Create a test block
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass activity record
+     */
+    abstract public function create_instance($record = null, array $options = null);
+}
index a1906a9..4a62f4c 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-// necessary when loaded from cli/util.php script
-// If this is missing then PHPUnit is not in your PHP include path. This normally
-// happens if installation didn't complete correctly. Check your environment.
 require_once 'PHPUnit/Autoload.php';
+require_once 'PHPUnit/Extensions/Database/Autoload.php';
 
 
 /**
@@ -44,20 +42,80 @@ class phpunit_util {
     protected static $tabledata = null;
 
     /**
-     * @var array An array of globals cloned from CFG
+     * @var array original structure of all database tables
+     */
+    protected static $tablestructure = null;
+
+    /**
+     * @var array An array of original globals, restored after each test
      */
     protected static $globals = array();
 
     /**
      * @var int last value of db writes counter, used for db resetting
      */
-    protected static $lastdbwrites = null;
+    public static $lastdbwrites = null;
 
     /**
      * @var phpunit_data_generator
      */
     protected static $generator = null;
 
+    /**
+     * @var resource used for prevention of parallel test execution
+     */
+    protected static $lockhandle = null;
+
+    /**
+     * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
+     *
+     * Note: do not call manually!
+     *
+     * @internal
+     * @static
+     * @return void
+     */
+    public static function acquire_test_lock() {
+        global $CFG;
+        if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
+            // dataroot not initialised yet
+            return;
+        }
+        if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) {
+            file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests');
+            phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock");
+        }
+        if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) {
+            $wouldblock = null;
+            $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock);
+            if (!$locked) {
+                if ($wouldblock) {
+                    echo "Waiting for other test execution to complete...\n";
+                }
+                $locked = flock(self::$lockhandle, LOCK_EX);
+            }
+            if (!$locked) {
+                fclose(self::$lockhandle);
+                self::$lockhandle = null;
+            }
+        }
+        register_shutdown_function(array('phpunit_util', 'release_test_lock'));
+    }
+
+    /**
+     * Note: do not call manually!
+     * @internal
+     * @static
+     * @return void
+     */
+    public static function release_test_lock() {
+        if (self::$lockhandle) {
+            flock(self::$lockhandle, LOCK_UN);
+            fclose(self::$lockhandle);
+            self::$lockhandle = null;
+        }
+    }
+
     /**
      * Get data generator
      * @static
@@ -90,118 +148,270 @@ class phpunit_util {
         }
 
         if (!is_array(self::$tabledata)) {
-            phpunit_bootstrap_error('Can not read dataroot/phpunit/tabledata.ser or invalid format!');
+            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
         }
 
         return self::$tabledata;
     }
 
     /**
-     * Reset all database tables to default values.
+     * Returns structure of all tables right after installation.
      * @static
-     * @param bool $logchanges
-     * @param null|PHPUnit_Framework_TestCase $caller
-     * @return bool true if reset done, false if skipped
+     * @return array $table=>$records
+     */
+    public static function get_tablestructure() {
+        global $CFG;
+
+        if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
+            // not initialised yet
+            return array();
+        }
+
+        if (!isset(self::$tablestructure)) {
+            $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
+            self::$tablestructure = unserialize($data);
+        }
+
+        if (!is_array(self::$tablestructure)) {
+            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.');
+        }
+
+        return self::$tablestructure;
+    }
+
+    /**
+     * Returns list of tables that are unmodified and empty.
+     *
+     * @static
+     * @return array of table names, empty if unknown
      */
-    public static function reset_database($logchanges = false, PHPUnit_Framework_TestCase $caller = null) {
+    protected static function guess_unmodified_empty_tables() {
         global $DB;
 
-        if ($logchanges) {
-            if (self::$lastdbwrites != $DB->perf_get_writes()) {
-                if ($caller) {
-                    $where = ' in testcase: '.get_class($caller).'->'.$caller->getName(true);
-                } else {
-                    $where = '';
+        $dbfamily = $DB->get_dbfamily();
+
+        if ($dbfamily === 'mysql') {
+            $empties = array();
+            $prefix = $DB->get_prefix();
+            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                if (!is_null($info->auto_increment)) {
+                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                    if ($info->auto_increment == 1) {
+                        $empties[$table] = $table;
+                    }
                 }
-                error_log('warning: unexpected database modification, resetting DB state'.$where);
             }
+            $rs->close();
+            return $empties;
+
+        } else if ($dbfamily === 'mssql') {
+            $empties = array();
+            $prefix = $DB->get_prefix();
+            $sql = "SELECT t.name
+                      FROM sys.identity_columns i
+                      JOIN sys.tables t ON t.object_id = i.object_id
+                     WHERE t.name LIKE ?
+                       AND i.name = 'id'
+                       AND i.last_value IS NULL";
+            $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                $empties[$table] = $table;
+            }
+            $rs->close();
+            return $empties;
+
+        } else {
+            return array();
         }
+    }
 
-        $tables = $DB->get_tables(false);
-        if (!$tables or empty($tables['config'])) {
-            // not installed yet
+    /**
+     * Reset all database sequences to initial values.
+     *
+     * @static
+     * @param array $empties tables that are known to be unmodified and empty
+     * @return void
+     */
+    public static function reset_all_database_sequences(array $empties = null) {
+        global $DB;
+
+        if (!$data = self::get_tabledata()) {
+            // not initialised yet
+            return;
+        }
+        if (!$structure = self::get_tablestructure()) {
+            // not initialised yet
             return;
         }
 
-        $dbreset = false;
-        if (is_null(self::$lastdbwrites) or self::$lastdbwrites != $DB->perf_get_writes()) {
-            if ($data = self::get_tabledata()) {