Merge branch 'wip-MDL-34012-master' of git://github.com/marinaglancy/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 2 Apr 2014 00:57:15 +0000 (02:57 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 2 Apr 2014 00:57:15 +0000 (02:57 +0200)
286 files changed:
admin/settings/security.php
admin/tool/log/lang/en/tool_log.php
admin/tool/task/classes/edit_scheduled_task_form.php
admin/tool/task/cli/schedule_task.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/scheduledtasks.php
auth/db/tests/db_test.php
auth/manual/auth.php
auth/manual/config.html
auth/manual/lang/en/auth_manual.php
auth/manual/tests/manual_test.php [new file with mode: 0644]
blog/tests/bloglib_test.php
cohort/tests/cohortlib_test.php
course/classes/editcategory_form.php
course/tests/behat/category_management.feature
course/tests/courselib_test.php
enrol/database/tests/sync_test.php
enrol/meta/tests/plugin_test.php
grade/edit/tree/grade.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/index.php
grade/report/grader/lib.php
group/tests/lib_test.php
lang/en/admin.php
lang/en/blog.php
lang/en/grades.php
lang/en/moodle.php
lang/en/notes.php
lang/en/webservice.php
lib/authlib.php
lib/behat/classes/behat_selectors.php
lib/classes/event/assessable_submitted.php
lib/classes/event/assessable_uploaded.php
lib/classes/event/base.php
lib/classes/event/blog_association_created.php
lib/classes/event/blog_entry_created.php
lib/classes/event/blog_entry_updated.php
lib/classes/event/cohort_deleted.php
lib/classes/event/comment_created.php
lib/classes/event/comment_deleted.php
lib/classes/event/content_viewed.php
lib/classes/event/course_category_created.php
lib/classes/event/course_category_updated.php
lib/classes/event/course_module_completion_updated.php
lib/classes/event/course_module_created.php
lib/classes/event/course_module_deleted.php
lib/classes/event/course_module_instance_list_viewed.php
lib/classes/event/course_module_updated.php
lib/classes/event/course_module_viewed.php
lib/classes/event/course_reset_ended.php
lib/classes/event/course_reset_started.php
lib/classes/event/course_updated.php
lib/classes/event/email_failed.php
lib/classes/event/group_member_added.php
lib/classes/event/group_member_removed.php
lib/classes/event/group_updated.php
lib/classes/event/grouping_created.php
lib/classes/event/grouping_deleted.php
lib/classes/event/grouping_updated.php
lib/classes/event/message_contact_added.php
lib/classes/event/message_contact_blocked.php
lib/classes/event/message_contact_removed.php
lib/classes/event/message_contact_unblocked.php
lib/classes/event/message_read.php
lib/classes/event/message_sent.php
lib/classes/event/mnet_access_control_created.php
lib/classes/event/mnet_access_control_updated.php
lib/classes/event/role_capabilities_updated.php
lib/classes/event/user_deleted.php
lib/classes/event/user_enrolment_updated.php
lib/classes/event/user_graded.php [new file with mode: 0644]
lib/classes/event/user_loggedin.php
lib/classes/event/user_login_failed.php
lib/classes/event/webservice_function_called.php
lib/classes/event/webservice_login_failed.php
lib/classes/event/webservice_service_updated.php
lib/classes/event/webservice_service_user_added.php
lib/classes/event/webservice_service_user_removed.php
lib/classes/event/webservice_token_created.php
lib/classes/task/manager.php
lib/classes/task/scheduled_task.php
lib/classes/task/send_failed_login_notifications_task.php
lib/classes/task/task_base.php
lib/classes/user.php
lib/datalib.php
lib/db/install.xml
lib/db/upgrade.php
lib/deprecatedlib.php
lib/dml/pgsql_native_moodle_database.php
lib/editor/atto/adminlib.php
lib/editor/atto/lang/en/editor_atto.php
lib/editor/atto/plugins/accessibilitychecker/lang/en/atto_accessibilitychecker.php
lib/editor/atto/plugins/backcolor/lang/en/atto_backcolor.php
lib/editor/atto/plugins/collapse/lang/en/atto_collapse.php
lib/editor/atto/plugins/equation/lang/en/atto_equation.php
lib/editor/atto/plugins/equation/lib.php
lib/editor/atto/plugins/equation/styles.css
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js
lib/editor/atto/plugins/equation/yui/src/button/js/button.js
lib/editor/atto/plugins/fontcolor/lang/en/atto_fontcolor.php
lib/editor/atto/plugins/image/lang/en/atto_image.php
lib/editor/atto/plugins/image/lib.php
lib/editor/atto/plugins/image/styles.css
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
lib/editor/atto/plugins/table/lang/en/atto_table.php
lib/editor/atto/plugins/table/lib.php
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-debug.js
lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-min.js
lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button.js
lib/editor/atto/plugins/undo/yui/src/button/js/button.js
lib/editor/atto/settings.php
lib/editor/atto/styles.css
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/editor/atto/yui/src/editor/js/editor-plugin-dialogue.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/editor/atto/yui/src/editor/js/selection.js
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/moodlelib.php
lib/outputrenderers.php
lib/testing/generator/data_generator.php
lib/tests/accesslib_test.php
lib/tests/adhoc_task_test.php
lib/tests/event_user_graded_test.php [new file with mode: 0644]
lib/tests/events_test.php
lib/tests/fixtures/event_fixtures.php
lib/tests/fixtures/task_fixtures.php [new file with mode: 0644]
lib/tests/scheduled_task_test.php
lib/tests/user_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-checknet/assets/checknet.txt [new file with mode: 0644]
lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-checknet/moodle-core-checknet.js [new file with mode: 0644]
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js
lib/yui/src/checknet/assets/checknet.txt [new file with mode: 0644]
lib/yui/src/checknet/build.json [new file with mode: 0644]
lib/yui/src/checknet/js/checknet.js [new file with mode: 0644]
lib/yui/src/checknet/meta/checknet.json [new file with mode: 0644]
lib/yui/src/chooserdialogue/js/chooserdialogue.js
message/tests/events_test.php
mnet/lib.php
mnet/tests/events_test.php
mod/assign/classes/event/all_submissions_downloaded.php
mod/assign/classes/event/assessable_submitted.php
mod/assign/classes/event/extension_granted.php
mod/assign/classes/event/identities_revealed.php
mod/assign/classes/event/marker_updated.php
mod/assign/classes/event/statement_accepted.php
mod/assign/classes/event/submission_created.php
mod/assign/classes/event/submission_duplicated.php
mod/assign/classes/event/submission_graded.php
mod/assign/classes/event/submission_locked.php
mod/assign/classes/event/submission_status_updated.php
mod/assign/classes/event/submission_unlocked.php
mod/assign/classes/event/submission_updated.php
mod/assign/classes/event/workflow_state_updated.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/feedback/editpdf/locallib.php
mod/assign/feedback/offline/locallib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/module.js
mod/assign/styles.css
mod/assign/submission/file/classes/event/assessable_uploaded.php
mod/chat/classes/event/message_sent.php
mod/chat/classes/event/sessions_viewed.php
mod/choice/classes/event/answer_submitted.php
mod/choice/classes/event/answer_updated.php
mod/choice/classes/event/report_viewed.php
mod/data/classes/event/field_created.php
mod/data/classes/event/field_deleted.php
mod/data/classes/event/field_updated.php
mod/data/classes/event/record_created.php
mod/data/classes/event/record_deleted.php
mod/data/classes/event/record_updated.php
mod/data/classes/event/template_updated.php
mod/data/classes/event/template_viewed.php
mod/data/tests/events_test.php
mod/forum/classes/event/assessable_uploaded.php
mod/glossary/classes/event/category_created.php
mod/glossary/classes/event/category_deleted.php
mod/glossary/classes/event/category_updated.php
mod/glossary/classes/event/entry_approved.php
mod/glossary/classes/event/entry_created.php
mod/glossary/classes/event/entry_deleted.php
mod/glossary/classes/event/entry_disapproved.php
mod/glossary/classes/event/entry_updated.php
mod/glossary/classes/event/entry_viewed.php
mod/glossary/sql.php
mod/lesson/classes/event/essay_assessed.php
mod/lti/lang/en/lti.php
mod/quiz/classes/event/attempt_abandoned.php
mod/quiz/classes/event/attempt_becameoverdue.php
mod/quiz/classes/event/attempt_started.php
mod/quiz/classes/event/attempt_submitted.php
mod/quiz/mod_form.php
mod/scorm/classes/event/interactions_viewed.php
mod/scorm/classes/event/report_viewed.php
mod/scorm/classes/event/sco_launched.php
mod/scorm/classes/event/user_report_viewed.php
mod/scorm/lang/en/scorm.php
mod/scorm/player.php
mod/survey/classes/event/course_module_viewed.php
mod/survey/classes/event/report_downloaded.php
mod/survey/classes/event/response_submitted.php
mod/wiki/backup/moodle2/backup_wiki_stepslib.php
mod/wiki/backup/moodle2/restore_wiki_stepslib.php
mod/wiki/classes/event/comments_viewed.php
mod/wiki/classes/event/page_diff_viewed.php
mod/wiki/classes/event/page_map_viewed.php
mod/wiki/classes/event/page_version_deleted.php
mod/wiki/classes/event/page_version_restored.php
mod/wiki/classes/event/page_version_viewed.php
mod/wiki/classes/event/page_viewed.php
mod/wiki/pagelib.php
mod/wiki/renderer.php
mod/workshop/classes/event/assessments_reset.php
mod/workshop/toolbox.php
question/addquestion.php
question/editlib.php
question/qbank.js [deleted file]
question/renderer.php
question/yui/build/moodle-question-chooser/moodle-question-chooser-debug.js [new file with mode: 0644]
question/yui/build/moodle-question-chooser/moodle-question-chooser-min.js [new file with mode: 0644]
question/yui/build/moodle-question-chooser/moodle-question-chooser.js [new file with mode: 0644]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js [new file with mode: 0644]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js [new file with mode: 0644]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js [new file with mode: 0644]
question/yui/src/chooser/build.json [new file with mode: 0644]
question/yui/src/chooser/js/chooser.js [new file with mode: 0644]
question/yui/src/chooser/meta/chooser.json [new file with mode: 0644]
question/yui/src/qbankmanager/build.json [new file with mode: 0644]
question/yui/src/qbankmanager/js/qbankmanager.js [new file with mode: 0644]
question/yui/src/qbankmanager/meta/qbankmanager.json [new file with mode: 0644]
report/log/classes/event/report_viewed.php
report/log/classes/event/user_report_viewed.php
report/log/tests/events_test.php
report/loglive/classes/event/report_viewed.php
report/loglive/tests/events_test.php
report/outline/classes/event/activity_viewed.php
report/outline/classes/event/outline_viewed.php
report/participation/classes/event/report_viewed.php
report/performance/locallib.php
report/stats/classes/event/report_viewed.php
report/stats/classes/event/user_report_viewed.php
tag/edit.php
theme/base/style/core.css
theme/base/style/question.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/style/moodle.css
theme/clean/pix/screenshot.jpg
theme/font.php
theme/more/db/install.php [new file with mode: 0644]
theme/more/db/upgrade.php [new file with mode: 0644]
theme/more/lang/en/theme_more.php
theme/more/pix/background.jpg [new file with mode: 0644]
theme/more/pix/screenshot.jpg
theme/more/settings.php
theme/more/version.php
user/lib.php
user/tests/userlib_test.php
version.php

index 62712e3..a75b6e1 100644 (file)
@@ -108,10 +108,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     // "notifications" settingpage
     $temp = new admin_settingpage('notifications', new lang_string('notifications', 'admin'));
-    $temp->add(new admin_setting_configselect('displayloginfailures', new lang_string('displayloginfailures', 'admin'), new lang_string('configdisplayloginfailures', 'admin'), '', array('' => new lang_string('nobody'),
-                                                                                                                                                                                'admin' => new lang_string('administrators'),
-                                                                                                                                                                                'teacher' => new lang_string('administratorsandteachers'),
-                                                                                                                                                                                'everybody' => new lang_string('everybody'))));
+    $temp->add(new admin_setting_configcheckbox('displayloginfailures', new lang_string('displayloginfailures', 'admin'),
+            new lang_string('configdisplayloginfailures', 'admin'), 0));
     $temp->add(new admin_setting_users_with_capability('notifyloginfailures', new lang_string('notifyloginfailures', 'admin'), new lang_string('confignotifyloginfailures', 'admin'), array(), 'moodle/site:config'));
     $options = array();
     for ($i = 1; $i <= 100; $i++) {
index ef20f85..62f4a48 100644 (file)
@@ -28,3 +28,5 @@ $string['logging'] = 'Logging';
 $string['managelogging'] = 'Manage log stores';
 $string['reportssupported'] = 'Reports supported';
 $string['pluginname'] = 'Log store manager';
+$string['subplugintype_logstore'] = 'Log store';
+$string['subplugintype_logstore_plural'] = 'Log stores';
index 43ff051..7b593ca 100644 (file)
@@ -34,42 +34,44 @@ require_once($CFG->libdir.'/formslib.php');
  */
 class tool_task_edit_scheduled_task_form extends moodleform {
     public function definition() {
-        global $CFG;
-
         $mform = $this->_form;
+        /** @var \core\task\scheduled_task $task */
         $task = $this->_customdata;
 
-        $never = get_string('never');
-        $none = get_string('none');
-        $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : $never;
-        $nextrun = $task->get_next_run_time() ? userdate($task->get_next_run_time()) : $none;
+        $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : get_string('never');
+        $nextrun = $task->get_next_run_time();
+        if ($task->get_disabled()) {
+            $nextrun = get_string('disabled', 'tool_task');
+        } else if ($nextrun > time()) {
+            $nextrun = userdate($nextrun);
+        } else {
+            $nextrun = get_string('asap', 'tool_task');
+        }
         $mform->addElement('static', 'lastrun', get_string('lastruntime', 'tool_task'), $lastrun);
         $mform->addElement('static', 'nextrun', get_string('nextruntime', 'tool_task'), $nextrun);
 
         $mform->addElement('text', 'minute', get_string('taskscheduleminute', 'tool_task'));
         $mform->setType('minute', PARAM_RAW);
         $mform->addHelpButton('minute', 'taskscheduleminute', 'tool_task');
-        $mform->setDefault('minute', $task->get_minute());
 
         $mform->addElement('text', 'hour', get_string('taskschedulehour', 'tool_task'));
         $mform->setType('hour', PARAM_RAW);
         $mform->addHelpButton('hour', 'taskschedulehour', 'tool_task');
-        $mform->setDefault('hour', $task->get_hour());
 
         $mform->addElement('text', 'day', get_string('taskscheduleday', 'tool_task'));
         $mform->setType('day', PARAM_RAW);
         $mform->addHelpButton('day', 'taskscheduleday', 'tool_task');
-        $mform->setDefault('day', $task->get_day());
 
         $mform->addElement('text', 'month', get_string('taskschedulemonth', 'tool_task'));
         $mform->setType('month', PARAM_RAW);
         $mform->addHelpButton('month', 'taskschedulemonth', 'tool_task');
-        $mform->setDefault('month', $task->get_month());
 
         $mform->addElement('text', 'dayofweek', get_string('taskscheduledayofweek', 'tool_task'));
         $mform->setType('dayofweek', PARAM_RAW);
         $mform->addHelpButton('dayofweek', 'taskscheduledayofweek', 'tool_task');
-        $mform->setDefault('dayofweek', $task->get_day_of_week());
+
+        $mform->addElement('advcheckbox', 'disabled', get_string('disabled', 'tool_task'));
+        $mform->addHelpButton('disabled', 'disabled', 'tool_task');
 
         $mform->addElement('advcheckbox', 'resettodefaults', get_string('resettasktodefaults', 'tool_task'));
         $mform->addHelpButton('resettodefaults', 'resettasktodefaults', 'tool_task');
@@ -79,12 +81,16 @@ class tool_task_edit_scheduled_task_form extends moodleform {
         $mform->disabledIf('day', 'resettodefaults', 'checked');
         $mform->disabledIf('dayofweek', 'resettodefaults', 'checked');
         $mform->disabledIf('month', 'resettodefaults', 'checked');
+        $mform->disabledIf('disabled', 'resettodefaults', 'checked');
 
         $mform->addElement('hidden', 'task', get_class($task));
         $mform->setType('task', PARAM_RAW);
         $mform->addElement('hidden', 'action', 'edit');
         $mform->setType('action', PARAM_ALPHANUMEXT);
         $this->add_action_buttons(true, get_string('savechanges'));
+
+        // Do not use defaults for existing values, the set_data() is the correct way.
+        $this->set_data(\core\task\manager::record_from_scheduled_task($task));
     }
 }
 
index 0b56ac5..0660eff 100644 (file)
@@ -70,7 +70,15 @@ if ($options['list']) {
             . $task->get_day_of_week() . ' '
             . $task->get_month() . ' '
             . $task->get_day_of_week();
-        $nextrun = $task->get_next_run_time() ? userdate($task->get_next_run_time(), $shorttime) : 'asap';
+        $nextrun = $task->get_next_run_time();
+
+        if ($task->get_disabled()) {
+            $nextrun = get_string('disabled', 'tool_task');
+        } else if ($nextrun > time()) {
+            $nextrun = userdate($nextrun);
+        } else {
+            $nextrun = get_string('asap', 'tool_task');
+        }
 
         echo str_pad($class, 50, ' ') . ' ' . str_pad($schedule, 17, ' ') . ' ' . $nextrun . "\n";
     }
index a4f5e72..493fa1d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['asap'] = 'ASAP';
 $string['blocking'] = 'Blocking';
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
 $string['default'] = 'Default';
+$string['disabled'] = 'Disabled';
+$string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
index 4a51116..f32c9e0 100644 (file)
@@ -57,11 +57,19 @@ class tool_task_renderer extends plugin_renderer_base {
         $yes = get_string('yes');
         $no = get_string('no');
         $never = get_string('never');
-        $now = get_string('now');
+        $asap = get_string('asap', 'tool_task');
+        $disabled = get_string('disabled', 'tool_task');
         foreach ($tasks as $task) {
             $customised = $task->is_customised() ? $no : $yes;
             $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : $never;
-            $nextrun = $task->get_next_run_time() ? userdate($task->get_next_run_time()) : $now;
+            $nextrun = $task->get_next_run_time();
+            if ($task->get_disabled()) {
+                $nextrun = $disabled;
+            } else if ($nextrun > time()) {
+                $nextrun = userdate($nextrun);
+            } else {
+                $nextrun = $asap;
+            }
             $configureurl = new moodle_url('/admin/tool/task/scheduledtasks.php', array('action'=>'edit', 'task' => get_class($task)));
             $editlink = $this->action_icon($configureurl, new pix_icon('t/edit', get_string('edittaskschedule', 'tool_task', $task->get_name())));
 
@@ -95,6 +103,9 @@ class tool_task_renderer extends plugin_renderer_base {
                         new html_table_cell($task->get_fail_delay()),
                         new html_table_cell($customised)));
 
+            if ($task->get_disabled()) {
+                $row->attributes['class'] = 'disabled';
+            }
             $data[] = $row;
         }
         $table->data = $data;
index 2779615..2b38300 100644 (file)
@@ -73,6 +73,7 @@ if ($mform && $mform->is_cancelled()) {
             $task->set_month($defaulttask->get_month());
             $task->set_day_of_week($defaulttask->get_day_of_week());
             $task->set_day($defaulttask->get_day());
+            $task->set_disabled($defaulttask->get_disabled());
             $task->set_customised(false);
         } else {
             $task->set_minute($data->minute);
@@ -80,6 +81,7 @@ if ($mform && $mform->is_cancelled()) {
             $task->set_month($data->month);
             $task->set_day_of_week($data->dayofweek);
             $task->set_day($data->day);
+            $task->set_disabled($data->disabled);
             $task->set_customised(true);
         }
 
index 9fccdd2..64f34f3 100644 (file)
@@ -87,7 +87,11 @@ class auth_db_testcase extends advanced_testcase {
                 set_config('sybasequoting', '0', 'auth/db');
                 if (!empty($CFG->dboptions['dbsocket']) and ($CFG->dbhost === 'localhost' or $CFG->dbhost === '127.0.0.1')) {
                     if (strpos($CFG->dboptions['dbsocket'], '/') !== false) {
-                        set_config('host', $CFG->dboptions['dbsocket'], 'auth/db');
+                        $socket = $CFG->dboptions['dbsocket'];
+                        if (!empty($CFG->dboptions['dbport'])) {
+                            $socket .= ':' . $CFG->dboptions['dbport'];
+                        }
+                        set_config('host', $socket, 'auth/db');
                     } else {
                         set_config('host', '', 'auth/db');
                     }
index 1c7ff9e..2803711 100644 (file)
@@ -37,12 +37,17 @@ require_once($CFG->libdir.'/authlib.php');
  */
 class auth_plugin_manual extends auth_plugin_base {
 
+    /**
+     * The name of the component. Used by the configuration.
+     */
+    const COMPONENT_NAME = 'auth_manual';
+
     /**
      * Constructor.
      */
     function auth_plugin_manual() {
         $this->authtype = 'manual';
-        $this->config = get_config('auth/manual');
+        $this->config = get_config(self::COMPONENT_NAME);
     }
 
     /**
@@ -81,6 +86,7 @@ class auth_plugin_manual extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        set_user_preference('auth_manual_passwordupdatetime', time(), $user->id);
         // This will also update the stored hash to the latest algorithm
         // if the existing hash is using an out-of-date algorithm (or the
         // legacy md5 algorithm).
@@ -153,13 +159,56 @@ class auth_plugin_manual extends auth_plugin_base {
         include 'config.html';
     }
 
+    /**
+     * Return number of days to user password expires.
+     *
+     * If user password does not expire, it should return 0 or a positive value.
+     * If user password is already expired, it should return negative value.
+     *
+     * @param mixed $username username (with system magic quotes)
+     * @return integer
+     */
+    public function password_expire($username) {
+        $result = 0;
+
+        if (!empty($this->config->expirationtime)) {
+            $user = core_user::get_user_by_username($username, 'id,timecreated');
+            $lastpasswordupdatetime = get_user_preferences('auth_manual_passwordupdatetime', $user->timecreated, $user->id);
+            $expiretime = $lastpasswordupdatetime + $this->config->expirationtime * DAYSECS;
+            $now = time();
+            $result = ($expiretime - $now) / DAYSECS;
+            if ($expiretime > $now) {
+                $result = ceil($result);
+            } else {
+                $result = floor($result);
+            }
+        }
+
+        return $result;
+    }
+
     /**
      * Processes and stores configuration data for this authentication plugin.
      *
-     * @param array $config
+     * @param stdClass $config
      * @return void
      */
     function process_config($config) {
+        // Set to defaults if undefined.
+        if (!isset($config->expiration)) {
+            $config->expiration = '';
+        }
+        if (!isset($config->expiration_warning)) {
+            $config->expiration_warning = '';
+        }
+        if (!isset($config->expirationtime)) {
+            $config->expirationtime = '';
+        }
+
+        // Save settings.
+        set_config('expiration', $config->expiration, self::COMPONENT_NAME);
+        set_config('expiration_warning', $config->expiration_warning, self::COMPONENT_NAME);
+        set_config('expirationtime', $config->expirationtime, self::COMPONENT_NAME);
         return true;
     }
 
index 8c03ce0..f622ab1 100644 (file)
@@ -1,5 +1,78 @@
-<table cellspacing="0" cellpadding="5" border="0">
 <?php
-print_auth_lock_options($this->authtype, $user_fields, get_string('auth_fieldlocks_help', 'auth'), false, false);
+    // Set to defaults if undefined.
+    if (!isset($config->expiration)) {
+        $config->expiration = '';
+    }
+    if (!isset($config->expiration_warning)) {
+        $config->expiration_warning = '';
+    }
+    if (!isset($config->expirationtime)) {
+        $config->expirationtime = '';
+    }
+    $expirationoptions = array(
+        new lang_string('no'),
+        new lang_string('yes'),
+    );
+    $expirationtimeoptions = array(
+        '30' => new lang_string('numdays', '', 30),
+        '60' => new lang_string('numdays', '', 60),
+        '90' => new lang_string('numdays', '', 90),
+        '120' => new lang_string('numdays', '', 120),
+        '150' => new lang_string('numdays', '', 150),
+        '180' => new lang_string('numdays', '', 180),
+        '365' => new lang_string('numdays', '', 365),
+    );
+    $expirationwarningoptions = array(
+        '0' => new lang_string('never'),
+        '1' => new lang_string('numdays', '', 1),
+        '2' => new lang_string('numdays', '', 2),
+        '3' => new lang_string('numdays', '', 3),
+        '4' => new lang_string('numdays', '', 4),
+        '5' => new lang_string('numdays', '', 5),
+        '6' => new lang_string('numdays', '', 6),
+        '7' => new lang_string('numdays', '', 7),
+        '10' => new lang_string('numdays', '', 10),
+        '14' => new lang_string('numdays', '', 14),
+    );
 ?>
+<table cellspacing="0" cellpadding="5" border="0">
+    <tr>
+        <td colspan="3">
+            <h3><?php print_string('passwdexpire_settings', 'auth_manual') ?></h3>
+        </td>
+    </tr>
+    <tr>
+        <td align="right">
+            <label for="menuexpiration">
+                <?php print_string('expiration', 'auth_manual') ?>
+            </label>
+        </td>
+        <td>
+            <?php echo html_writer::select($expirationoptions, 'expiration', $config->expiration, false) ?>
+        </td>
+        <td><?php print_string('expiration_desc', 'auth_manual') ?></td>
+    </tr>
+    <tr>
+        <td align="right">
+            <label for="menuexpirationtime">
+                <?php print_string('passwdexpiretime', 'auth_manual') ?>
+            </label>
+        </td>
+        <td>
+            <?php echo html_writer::select($expirationtimeoptions, 'expirationtime', $config->expirationtime, false) ?>
+        </td>
+        <td><?php print_string('passwdexpiretime_desc', 'auth_manual') ?></td>
+    </tr>
+    <tr>
+        <td align="right">
+            <label for="menuexpiration_warning">
+                <?php print_string('expiration_warning', 'auth_manual') ?>
+            </label>
+        </td>
+        <td>
+            <?php echo html_writer::select($expirationwarningoptions, 'expiration_warning', $config->expiration_warning, false) ?>
+        </td>
+        <td><?php print_string('expiration_warning_desc', 'auth_manual') ?></td>
+    </tr>
+    <?php print_auth_lock_options($this->authtype, $user_fields, get_string('auth_fieldlocks_help', 'auth'), false, false) ?>
 </table>
index 53d8909..a919797 100644 (file)
  */
 
 $string['auth_manualdescription'] = 'This method removes any way for users to create their own accounts.  All accounts must be manually created by the admin user.';
+$string['expiration'] = 'Enable password expiry';
+$string['expiration_desc'] = 'Allow passwords to expire after a specified time.';
+$string['expiration_warning'] = 'Notification threshold';
+$string['expiration_warning_desc'] = 'Number of days before password expiry that a notification is issued.';
+$string['passwdexpiretime'] = 'Password duration';
+$string['passwdexpiretime_desc'] = 'Length of time for which a password is valid.';
 $string['pluginname'] = 'Manual accounts';
+$string['passwdexpire_settings'] = 'Password expiry settings';
diff --git a/auth/manual/tests/manual_test.php b/auth/manual/tests/manual_test.php
new file mode 100644 (file)
index 0000000..087721a
--- /dev/null
@@ -0,0 +1,108 @@
+<?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/>.
+
+/**
+ * Manual authentication tests.
+ *
+ * @package    auth_manual
+ * @category   test
+ * @copyright  2014 Gilles-Philippe Leblanc <gilles-philippe.leblanc@umontreal.ca>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/auth/manual/auth.php');
+
+/**
+ * Manual authentication tests class.
+ *
+ * @package    auth_manual
+ * @category   test
+ * @copyright  2014 Gilles-Philippe Leblanc <gilles-philippe.leblanc@umontreal.ca>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class auth_manual_testcase extends advanced_testcase {
+
+    /** @var auth_plugin_manual Keeps the authentication plugin. */
+    protected $authplugin;
+
+    /** @var stdClass Keeps authentication plugin config */
+    protected $config;
+
+    /**
+     * Setup test data.
+     */
+    protected function setUp() {
+        $this->resetAfterTest(true);
+        $this->authplugin = new auth_plugin_manual();
+        $this->config = new stdClass();
+        $this->config->expiration = '1';
+        $this->config->expiration_warning = '2';
+        $this->config->expirationtime = '30';
+        $this->authplugin->process_config($this->config);
+        $this->authplugin->config = get_config(auth_plugin_manual::COMPONENT_NAME);
+    }
+
+    /**
+     * Test user_update_password method.
+     */
+    public function test_user_update_password() {
+        $user = $this->getDataGenerator()->create_user();
+        $expectedtime = time();
+        $passwordisupdated = $this->authplugin->user_update_password($user, 'MyNewPassword*');
+
+        // Assert that the actual time should be equal or a little greater than the expected time.
+        $this->assertGreaterThanOrEqual($expectedtime, get_user_preferences('auth_manual_passwordupdatetime', 0, $user->id));
+
+        // Assert that the password was successfully updated.
+        $this->assertTrue($passwordisupdated);
+    }
+
+    /**
+     * Test test_password_expire method.
+     */
+    public function test_password_expire() {
+        $userrecord = array();
+        $expirationtime = 31 * DAYSECS;
+        $userrecord['timecreated'] = time() - $expirationtime;
+        $user1 = $this->getDataGenerator()->create_user($userrecord);
+        $user2 = $this->getDataGenerator()->create_user();
+
+        // The user 1 was created 31 days ago and has not changed his password yet, so the password has expirated.
+        $this->assertLessThanOrEqual(-1, $this->authplugin->password_expire($user1->username));
+
+        // The user 2 just came to be created and has not changed his password yet, so the password has not expirated.
+        $this->assertEquals(30, $this->authplugin->password_expire($user2->username));
+
+        $this->authplugin->user_update_password($user1, 'MyNewPassword*');
+
+        // The user 1 just updated his password so the password has not expirated.
+        $this->assertEquals(30, $this->authplugin->password_expire($user1->username));
+    }
+
+    /**
+     * Test test_process_config method.
+     */
+    public function test_process_config() {
+        $this->assertTrue($this->authplugin->process_config($this->config));
+        $config = get_config(auth_plugin_manual::COMPONENT_NAME);
+        $this->assertEquals($this->config->expiration, $config->expiration);
+        $this->assertEquals($this->config->expiration_warning, $config->expiration_warning);
+        $this->assertEquals($this->config->expirationtime, $config->expirationtime);
+    }
+}
index b4af764..87c78bb 100644 (file)
@@ -174,6 +174,8 @@ class core_bloglib_testcase extends advanced_testcase {
 
         // Validate event data.
         $this->assertInstanceOf('\core\event\blog_entry_created', $event);
+        $url = new moodle_url('/blog/index.php', array('entryid' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($sitecontext->id, $event->contextid);
         $this->assertEquals($blog->id, $event->objectid);
         $this->assertEquals($USER->id, $event->userid);
@@ -207,6 +209,8 @@ class core_bloglib_testcase extends advanced_testcase {
 
         // Validate event data.
         $this->assertInstanceOf('\core\event\blog_entry_updated', $event);
+        $url = new moodle_url('/blog/index.php', array('entryid' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($sitecontext->id, $event->contextid);
         $this->assertEquals($blog->id, $event->objectid);
         $this->assertEquals($USER->id, $event->userid);
@@ -240,6 +244,7 @@ class core_bloglib_testcase extends advanced_testcase {
 
         // Validate event data.
         $this->assertInstanceOf('\core\event\blog_entry_deleted', $event);
+        $this->assertEquals(null, $event->get_url());
         $this->assertEquals($sitecontext->id, $event->contextid);
         $this->assertEquals($blog->id, $event->objectid);
         $this->assertEquals($USER->id, $event->userid);
@@ -278,6 +283,8 @@ class core_bloglib_testcase extends advanced_testcase {
         // Validate event data.
         $this->assertInstanceOf('\core\event\blog_association_created', $event);
         $this->assertEquals($sitecontext->id, $event->contextid);
+        $url = new moodle_url('/blog/index.php', array('entryid' => $event->other['blogid']));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($blog->id, $event->other['blogid']);
         $this->assertEquals($this->courseid, $event->other['associateid']);
         $this->assertEquals('course', $event->other['associatetype']);
index 6392b84..eadcc56 100644 (file)
@@ -104,6 +104,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals('cohort', $event->objecttable);
         $this->assertEquals($id, $event->objectid);
         $this->assertEquals($cohort->contextid, $event->contextid);
+        $url = new moodle_url('/cohort/index.php', array('contextid' => $event->contextid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
         $this->assertEventLegacyData($cohort, $event);
         $this->assertEventContextNotUsed($event);
@@ -175,6 +177,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals('cohort', $event->objecttable);
         $this->assertEquals($updatedcohort->id, $event->objectid);
         $this->assertEquals($updatedcohort->contextid, $event->contextid);
+        $url = new moodle_url('/cohort/edit.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
         $this->assertEventLegacyData($cohort, $event);
         $this->assertEventContextNotUsed($event);
@@ -213,6 +217,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertInstanceOf('\core\event\cohort_deleted', $event);
         $this->assertEquals('cohort', $event->objecttable);
         $this->assertEquals($cohort->id, $event->objectid);
+        $url = new moodle_url('/cohort/index.php', array('contextid' => $event->contextid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $cohort->id));
         $this->assertEventLegacyData($cohort, $event);
         $this->assertEventContextNotUsed($event);
@@ -272,6 +278,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->id, $event->objectid);
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals($USER->id, $event->userid);
+        $url = new moodle_url('/cohort/assign.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
         $this->assertEventContextNotUsed($event);
     }
@@ -316,6 +324,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->id, $event->objectid);
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals($USER->id, $event->userid);
+        $url = new moodle_url('/cohort/assign.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
         $this->assertEventContextNotUsed($event);
     }
index a28ea9b..c866f6c 100644 (file)
@@ -125,10 +125,10 @@ class core_course_editcategory_form extends moodleform {
         if (!empty($data['idnumber'])) {
             if ($existing = $DB->get_record('course_categories', array('idnumber' => $data['idnumber']))) {
                 if (!$data['id'] || $existing->id != $data['id']) {
-                    $errors['idnumber']= get_string('categoryidnumbertaken');
+                    $errors['idnumber'] = get_string('categoryidnumbertaken', 'error');
                 }
             }
         }
         return $errors;
     }
-}
\ No newline at end of file
+}
index e624efc..764794a 100644 (file)
@@ -332,3 +332,17 @@ Feature: Test category management actions
     And the "movecategoriesto" "select" should be disabled
     And the "resortcategoriesby" "select" should be disabled
     And the "resortcoursesby" "select" should be disabled
+
+  Scenario: Test that is not possible to create a course category with a duplicate idnumber
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Courses" node
+    And I follow "Add a category"
+    And I set the following fields to these values:
+      | Category name | Test duplicate |
+      | Category ID number | CAT1 |
+    When I press "Create category"
+    Then I should see "ID number is already used for another category"
index 45c731c..0334214 100644 (file)
@@ -1496,6 +1496,8 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals('course', $event->objecttable);
         $this->assertEquals($updatedcourse->id, $event->objectid);
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $url = new moodle_url('/course/edit.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($updatedcourse, $event->get_record_snapshot('course', $event->objectid));
         $this->assertEquals('course_updated', $event->get_legacy_eventname());
         $this->assertEventLegacyData($updatedcourse, $event);
@@ -1666,6 +1668,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals($category->id, $event->objectid);
         $this->assertEquals($categorycontext->id, $event->contextid);
         $this->assertEquals('course_category_deleted', $event->get_legacy_eventname());
+        $this->assertEquals(null, $event->get_url());
         $this->assertEventLegacyData($category, $event);
         $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category->name . '(ID ' . $category->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
@@ -1762,6 +1765,8 @@ class core_course_courselib_testcase extends advanced_testcase {
             'operation' => $rc->get_operation(),
             'samesite' => $rc->is_samesite()
         );
+        $url = new moodle_url('/course/view.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEventLegacyData($legacydata, $event);
         $this->assertEventContextNotUsed($event);
 
@@ -1813,6 +1818,8 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals($coursecontext->id, $event->contextid);
         $expecteddesc = 'Course ' . $event->courseid . ' section ' . $event->other['sectionnum'] . ' updated by user ' . $event->userid;
         $this->assertEquals($expecteddesc, $event->get_description());
+        $url = new moodle_url('/course/editsection.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
         $id = $section->id;
         $sectionnum = $section->section;
index 9e73cc9..30fdc44 100644 (file)
@@ -89,7 +89,11 @@ class enrol_database_testcase extends advanced_testcase {
                 set_config('dbsybasequoting', '0', 'enrol_database');
                 if (!empty($CFG->dboptions['dbsocket']) and ($CFG->dbhost === 'localhost' or $CFG->dbhost === '127.0.0.1')) {
                     if (strpos($CFG->dboptions['dbsocket'], '/') !== false) {
-                      set_config('dbhost', $CFG->dboptions['dbsocket'], 'enrol_database');
+                        $socket = $CFG->dboptions['dbsocket'];
+                        if (!empty($CFG->dboptions['dbport'])) {
+                            $socket .= ':' . $CFG->dboptions['dbport'];
+                        }
+                        set_config('dbhost', $socket, 'enrol_database');
                     } else {
                       set_config('dbhost', '', 'enrol_database');
                     }
index a23c148..3a9e038 100644 (file)
@@ -547,6 +547,8 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $expectedlegacyeventdata = $dbuserenrolled;
         $expectedlegacyeventdata->enrol = 'meta';
         $expectedlegacyeventdata->courseid = $course2->id;
+        $url = new \moodle_url('/enrol/editenrolment.php', array('ue' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
         $this->assertEventContextNotUsed($event);
     }
index 3545a64..62536f6 100644 (file)
@@ -197,17 +197,6 @@ if ($mform->is_cancelled()) {
         $data->feedbackformat = $old_grade_grade->feedbackformat;
     }
 
-    // Only log a grade override if they actually changed the student grade.
-    if ($data->finalgrade != $old_grade_grade->finalgrade) {
-        $url = '/report/grader/index.php?id=' . $course->id;
-
-        $user = $DB->get_record('user', array('id'=>$data->userid), '*', MUST_EXIST);
-        $fullname = fullname($user);
-
-        $info = "{$grade_item->itemname}: $fullname";
-        add_to_log($course->id, 'grade', 'update', $url, $info);
-    }
-
     // update final grade or feedback
     // when we set override grade the first time, it happens here
     $grade_item->update_final_grade($data->userid, $data->finalgrade, 'editgrade', $data->feedback, $data->feedbackformat);
@@ -221,19 +210,6 @@ if ($mform->is_cancelled()) {
             $data->overridden = 0; // checkbox unticked
         }
         $grade_grade->set_overridden($data->overridden);
-
-        if ($data->overridden == 0 && $data->overridden != $old_grade_grade->overridden) {
-            // Log removing an override.
-            // The addition of an override is logged above.
-            // One or the other will happen but never both.
-            $url = '/report/grader/index.php?id=' . $course->id;
-
-            $user = $DB->get_record('user', array('id'=>$data->userid), '*', MUST_EXIST);
-            $fullname = fullname($user);
-
-            $info = "{$grade_item->itemname}: $fullname";
-            add_to_log($course->id, 'grade', 'update', $url, $info);
-        }
     }
 
     if (has_capability('moodle/grade:manage', $context) or has_capability('moodle/grade:hide', $context)) {
@@ -289,6 +265,14 @@ if ($mform->is_cancelled()) {
         $grade_item->force_regrading();
     }
 
+    $grade_grade = new grade_grade(array('userid'=>$data->userid, 'itemid'=>$grade_item->id), true);
+    if ($old_grade_grade->finalgrade != $grade_grade->finalgrade
+        or empty($old_grade_grade->overridden) != empty($grade_grade->overridden)
+    ) {
+        $grade_grade->grade_item = $grade_item;
+        \core\event\user_graded::create_from_grade($grade_grade)->trigger();
+    }
+
     redirect($returnurl);
 }
 
index b61f579..d49e265 100644 (file)
@@ -118,16 +118,10 @@ switch ($action) {
                 echo json_encode($json_object);
                 die();
             } else {
-                $url = '/report/grader/index.php?id=' . $course->id;
-
-                $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
-                $fullname = fullname($user);
-
-                $info = "{$grade_item->itemname}: $fullname";
-                add_to_log($course->id, 'grade', 'update', $url, $info);
-
                 $json_object->gradevalue = $finalvalue;
 
+                $old_grade_grade = new grade_grade(array('userid' => $userid, 'itemid' => $grade_item->id), true);
+
                 if ($grade_item->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE)) {
                     $json_object->result = 'success';
                     $json_object->message = false;
@@ -138,6 +132,14 @@ switch ($action) {
                     die();
                 }
 
+                $grade_grade = new grade_grade(array('userid' => $userid, 'itemid' => $grade_item->id), true);
+                if ($old_grade_grade->finalgrade != $grade_grade->finalgrade
+                    or empty($old_grade_grade->overridden) != empty($grade_grade->overridden)
+                ) {
+                    $grade_grade->load_grade_item();
+                    \core\event\user_graded::create_from_grade($grade_grade)->trigger();
+                }
+
                 // Get row data
                 $sql = "SELECT gg.id, gi.id AS itemid, gi.scaleid AS scale, gg.userid AS userid, finalgrade, gg.overridden AS overridden "
                      . "FROM {grade_grades} gg, {grade_items} gi WHERE "
index b3d812a..c73e1f0 100644 (file)
@@ -179,7 +179,7 @@ $reporthtml = $report->get_grade_table($displayaverages);
 
 // print submit button
 if ($USER->gradeediting[$course->id] && ($report->get_pref('showquickfeedback') || $report->get_pref('quickgrading'))) {
-    echo '<form action="index.php" enctype="application/x-www-form-urlencoded" method="post">'; // Enforce compatibility with our max_input_vars hack.
+    echo '<form action="index.php" enctype="application/x-www-form-urlencoded" method="post" id="gradereport_grader">'; // Enforce compatibility with our max_input_vars hack.
     echo '<div>';
     echo '<input type="hidden" value="'.s($courseid).'" name="id" />';
     echo '<input type="hidden" value="'.sesskey().'" name="sesskey" />';
index f4a357c..c91394a 100644 (file)
@@ -301,14 +301,19 @@ class grade_report_grader extends grade_report {
                         }
                     }
 
-                    $url = '/report/grader/index.php?id=' . $this->course->id;
-                    $fullname = fullname($this->users[$userid]);
-
-                    $info = "{$gradeitem->itemname}: $fullname";
-                    add_to_log($this->course->id, 'grade', 'update', $url, $info);
+                    $oldgradegrade = new grade_grade(array('userid' => $userid, 'itemid' => $gradeitem->id), true);
 
                     $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
 
+                    $gradegrade = new grade_grade(array('userid' => $userid, 'itemid' => $gradeitem->id), true);
+
+                    if ($oldgradegrade->finalgrade != $gradegrade->finalgrade
+                        or empty($oldgradegrade->overridden) != empty($gradegrade->overridden)
+                    ) {
+                        $gradegrade->grade_item = $gradeitem;
+                        \core\event\user_graded::create_from_grade($gradegrade)->trigger();
+                    }
+
                     // We can update feedback without reloading the grade item as it doesn't affect grade calculations
                     if ($datatype === 'feedback') {
                         $this->grades[$userid][$itemid]->feedback = $feedback;
@@ -1056,6 +1061,15 @@ class grade_report_grader extends grade_report {
         $PAGE->requires->js_init_call('M.gradereport_grader.init_report', $jsarguments, false, $module);
         $PAGE->requires->strings_for_js(array('addfeedback', 'feedback', 'grade'), 'grades');
         $PAGE->requires->strings_for_js(array('ajaxchoosescale', 'ajaxclicktoclose', 'ajaxerror', 'ajaxfailedupdate', 'ajaxfieldchanged'), 'gradereport_grader');
+        if (!$this->get_pref('enableajax') && $USER->gradeediting[$this->courseid]) {
+            $PAGE->requires->yui_module('moodle-core-formchangechecker',
+                    'M.core_formchangechecker.init',
+                    array(array(
+                        'formid' => 'gradereport_grader'
+                    ))
+            );
+            $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
+        }
 
         $rows = $this->get_right_range_row($rows);
         if ($displayaverages) {
index d845868..4f7dd52 100644 (file)
@@ -62,6 +62,8 @@ class core_group_lib_testcase extends advanced_testcase {
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/members.php', array('group' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_member_removed_event() {
@@ -89,6 +91,8 @@ class core_group_lib_testcase extends advanced_testcase {
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/members.php', array('group' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_group_created_event() {
@@ -108,6 +112,8 @@ class core_group_lib_testcase extends advanced_testcase {
         $this->assertSame('groups_group_created', $event->get_legacy_eventname());
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/index.php', array('id' => $event->courseid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_grouping_created_event() {
@@ -129,6 +135,8 @@ class core_group_lib_testcase extends advanced_testcase {
 
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/groupings.php', array('id' => $event->courseid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_group_updated_event() {
@@ -158,6 +166,8 @@ class core_group_lib_testcase extends advanced_testcase {
         $this->assertSame('groups_group_updated', $event->get_legacy_eventname());
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/group.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_grouping_updated_event() {
@@ -196,6 +206,8 @@ class core_group_lib_testcase extends advanced_testcase {
 
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($grouping->id, $event->objectid);
+        $url = new moodle_url('/group/grouping.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_group_deleted_event() {
@@ -215,6 +227,8 @@ class core_group_lib_testcase extends advanced_testcase {
         $this->assertSame('groups_group_deleted', $event->get_legacy_eventname());
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/index.php', array('id' => $event->courseid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_grouping_deleted_event() {
@@ -234,6 +248,8 @@ class core_group_lib_testcase extends advanced_testcase {
         $this->assertSame('groups_grouping_deleted', $event->get_legacy_eventname());
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
         $this->assertEquals($group->id, $event->objectid);
+        $url = new moodle_url('/group/groupings.php', array('id' => $event->courseid));
+        $this->assertEquals($url, $event->get_url());
     }
 
     public function test_groups_delete_group_members() {
index 8221742..e7afe89 100644 (file)
@@ -190,7 +190,7 @@ $string['configdenyemailaddresses'] = 'To deny email addresses from particular d
 $string['configenableblogs'] = 'This switch provides all site users with their own blog.';
 $string['configenabledevicedetection'] = 'Enables detection of mobiles, smartphones, tablets or default devices (desktop PCs, laptops, etc) for the application of themes and other features.';
 $string['configdisableuserimages'] = 'Disable the ability for users to change user profile images.';
-$string['configdisplayloginfailures'] = 'This will display information to selected users about previous failed logins.';
+$string['configdisplayloginfailures'] = 'This will display information to users about previous failed logins.';
 $string['configdndallowtextandlinks'] = 'Enable or disable the dragging and dropping of text and links onto a course page, alongside the dragging and dropping of files. Note that the dragging of text into Firefox or between different browsers is unreliable and may result in no data being uploaded, or corrupted text being uploaded.';
 $string['configdoclang'] = 'This language will be used in links for the documentation pages.';
 $string['configdocroot'] = 'Defines the path to the Moodle Docs for providing context-specific documentation via \'Moodle Docs for this page\' links in the footer of each page. If the field is left blank, links will not be displayed.';
@@ -428,7 +428,7 @@ $string['devicedetectregexvalue'] = 'Return value';
 $string['devicetype'] = 'Device type';
 $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
-$string['displayloginfailures'] = 'Display login failures to';
+$string['displayloginfailures'] = 'Display login failures';
 $string['dndallowtextandlinks'] = 'Drag and drop upload of text/links';
 $string['doclang'] = 'Language for docs';
 $string['docroot'] = 'Moodle Docs document root';
index a7d5d29..0904968 100644 (file)
@@ -87,7 +87,7 @@ $string['entryerrornotyours'] = 'This entry is not yours';
 $string['entrysaved'] = 'Your entry has been saved';
 $string['entrytitle'] = 'Entry title';
 $string['eventblogentriesviewed'] = 'Blog entries viewed';
-$string['eventblogassociationcreated'] = 'Blog association created';
+$string['eventblogassociationadded'] = 'Blog association created';
 $string['evententryadded'] = 'Blog entry added';
 $string['evententrydeleted'] = 'Blog entry deleted';
 $string['evententryupdated'] = 'Blog entry updated';
index 005f649..fc20033 100644 (file)
@@ -185,6 +185,7 @@ $string['errorupdatinggradecategoryaggregateoutcomes'] = 'Error updating the "In
 $string['errorupdatinggradecategoryaggregatesubcats'] = 'Error updating the "Aggregate including subcategories" setting of grade category ID {$a->id}';
 $string['errorupdatinggradecategoryaggregation'] = 'Error updating the aggregation type of grade category ID {$a->id}';
 $string['errorupdatinggradeitemaggregationcoef'] = 'Error updating the aggregation coefficient (weight or extra credit) of grade item ID {$a->id}';
+$string['eventusergraded'] = 'User grade edited in gradebook';
 $string['excluded'] = 'Excluded';
 $string['excluded_help'] = 'If ticked, the grade will not be included in any aggregation.';
 $string['expand'] = 'Expand category';
index 2e01aab..469ff00 100644 (file)
@@ -762,7 +762,6 @@ $string['explanation'] = 'Explanation';
 $string['extendenrol'] = 'Extend enrolment (individual)';
 $string['extendperiod'] = 'Extended period';
 $string['failedloginattempts'] = '{$a->attempts} failed logins since your last login';
-$string['failedloginattemptsall'] = '{$a->attempts} failed logins for {$a->accounts} accounts';
 $string['feedback'] = 'Feedback';
 $string['file'] = 'File';
 $string['fileexists'] = 'There is already a file called {$a}';
@@ -1210,6 +1209,8 @@ $string['nameforlink'] = 'What do you want to call this link?';
 $string['nameforpage'] = 'Name';
 $string['navigation'] = 'Navigation';
 $string['needed'] = 'Needed';
+$string['networkdropped'] = 'We have detected that your Internet connection is unreliable or has been interrupted.<br />
+Please be aware that changes may not be saved properly until your connection improves.';
 $string['never'] = 'Never';
 $string['neverdeletelogs'] = 'Never delete logs';
 $string['new'] = 'New';
@@ -1324,7 +1325,7 @@ $string['notice'] = 'Notice';
 $string['noticenewerbackup'] = 'This backup file has been created with Moodle {$a->backuprelease} ({$a->backupversion}) and it\'s newer than your currently installed Moodle {$a->serverrelease} ({$a->serverversion}). This could cause some inconsistencies because backwards compatibility of backup files cannot be guaranteed.';
 $string['notifications'] = 'Notifications';
 $string['notifyloginfailuresmessage'] = '{$a->time}, IP: {$a->ip}, User: {$a->info}';
-$string['notifyloginfailuresmessageend'] = 'You can view these logs at {$a}/report/log/index.php?id=1&amp;chooselog=1&amp;modid=site_errors.';
+$string['notifyloginfailuresmessageend'] = 'You can view these logs at {$a}';
 $string['notifyloginfailuresmessagestart'] = 'Here is a list of failed login attempts at {$a} since you were last notified';
 $string['notifyloginfailuressubject'] = '{$a} :: Failed logins notification';
 $string['notincluded'] = 'Not included';
index 84b8489..48aab57 100644 (file)
@@ -37,7 +37,7 @@ $string['deletenotes'] = 'Delete all notes';
 $string['editnote'] = 'Edit note';
 $string['enablenotes'] = 'Enable notes';
 $string['eventnotecreated'] = 'Note created';
-$string['eventnoteupdate'] = 'Note updated';
+$string['eventnoteupdated'] = 'Note updated';
 $string['eventnotedeleted'] = 'Note deleted';
 $string['eventnotesviewed'] = 'Notes viewed';
 $string['groupaddnewnote'] = 'Add a common note';
index 568111e..00a61ed 100644 (file)
@@ -85,6 +85,7 @@ $string['erroroptionalparamarray'] = 'The web service description parameter name
 $string['event_webservice_function_called'] = 'Web service function called';
 $string['event_webservice_login_failed'] = 'Web service login failed';
 $string['event_webservice_service_created'] = 'Web service service created';
+$string['event_webservice_service_deleted'] = 'Web service service deleted';
 $string['event_webservice_service_updated'] = 'Web service service updated';
 $string['event_webservice_service_user_added'] = 'Web service service user added';
 $string['event_webservice_service_user_removed'] = 'Web service service user removed';
index 639d2f0..92ac007 100644 (file)
@@ -647,6 +647,12 @@ function login_attempt_failed($user) {
         return;
     }
 
+    $count = get_user_preferences('login_failed_count', 0, $user);
+    $last = get_user_preferences('login_failed_last', 0, $user);
+    $sincescuccess = get_user_preferences('login_failed_count_since_success', $count, $user);
+    $sincescuccess = $sincescuccess + 1;
+    set_user_preference('login_failed_count_since_success', $sincescuccess, $user);
+
     if (empty($CFG->lockoutthreshold)) {
         // No threshold means no lockout.
         // Always unlock here, there might be some race conditions or leftovers when switching threshold.
@@ -654,9 +660,6 @@ function login_attempt_failed($user) {
         return;
     }
 
-    $count = get_user_preferences('login_failed_count', 0, $user);
-    $last = get_user_preferences('login_failed_last', 0, $user);
-
     if (!empty($CFG->lockoutwindow) and time() - $last > $CFG->lockoutwindow) {
         $count = 0;
     }
index d9d9246..cedf70e 100644 (file)
@@ -83,7 +83,12 @@ class behat_selectors {
      */
     protected static $moodleselectors = array(
         'dialogue' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')]/descendant::h1[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')] | //div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ')]/descendant::div[@class='hd'][normalize-space(.) = %locator%]/parent::div
+//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
+    normalize-space(descendant::div[
+        contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hd ')
+        ]) = %locator%] |
+//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
+    normalize-space(descendant::div[@class='hd']) = %locator%]
 XPATH
         , 'block' => <<<XPATH
 //div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' '))] | //div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]/descendant::h2[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]
index 8b632e3..6b4bf6e 100644 (file)
@@ -59,6 +59,7 @@ abstract class assessable_submitted extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if ($this->contextlevel != CONTEXT_MODULE) {
             throw new \coding_exception('Context passed must be module context.');
         }
index b284f99..972195c 100644 (file)
@@ -66,6 +66,7 @@ abstract class assessable_uploaded extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if ($this->contextlevel != CONTEXT_MODULE) {
             throw new \coding_exception('Context passed must be module context.');
         } else if (!isset($this->other['pathnamehashes']) || !is_array($this->other['pathnamehashes'])) {
index 58329d9..9178561 100644 (file)
@@ -345,6 +345,7 @@ abstract class base implements \IteratorAggregate {
             return false;
         }
 
+        $event->init(); // Init method of events could be setting custom properties.
         $event->restored = true;
         $event->triggered = true;
         $event->dispatched = true;
index 0fa689b..28c03c1 100644 (file)
@@ -78,7 +78,7 @@ class blog_association_created extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/blog/index.php', array('entryid' => $this->objectid, 'userid' => $this->userid));
+        return new \moodle_url('/blog/index.php', array('entryid' => $this->other['blogid']));
     }
 
     /**
@@ -103,6 +103,7 @@ class blog_association_created extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (empty($this->other['associatetype']) || ($this->other['associatetype'] !== 'course'
                 && $this->other['associatetype'] !== 'coursemodule')) {
             throw new \coding_exception('Invalid associatetype in event blog_association_created.');
index 4df80ac..1b3f6b1 100644 (file)
@@ -94,7 +94,7 @@ class blog_entry_created extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/blog/index.php', array('entryid' => $this->objectid, 'userid' => $this->userid));
+        return new \moodle_url('/blog/index.php', array('entryid' => $this->objectid));
     }
 
     /**
index 1ef826f..ff9686b 100644 (file)
@@ -92,7 +92,7 @@ class blog_entry_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/blog/index.php', array('entryid' => $this->objectid, 'userid' => $this->userid));
+        return new \moodle_url('/blog/index.php', array('entryid' => $this->objectid));
     }
 
     /**
index 0844565..ccc2ccd 100644 (file)
@@ -51,7 +51,7 @@ class cohort_deleted extends base {
      * @return string
      */
     public static function get_name() {
-        return get_string('event_core_deleted', 'core_cohort');
+        return get_string('event_cohort_deleted', 'core_cohort');
     }
 
     /**
index 420c4b8..2e18f69 100644 (file)
@@ -94,6 +94,7 @@ abstract class comment_created extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['itemid'])) {
             throw new \coding_exception('The itemid needs to be set in $other');
         }
index 57084fb..be04218 100644 (file)
@@ -94,6 +94,7 @@ abstract class comment_deleted extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['itemid'])) {
             throw new \coding_exception('The itemid needs to be set in $other');
         }
index d470e37..797ad37 100644 (file)
@@ -113,11 +113,10 @@ abstract class content_viewed extends base {
      * @return void
      */
     protected function validate_data() {
-        if (debugging('', DEBUG_DEVELOPER)) {
-            // Make sure this class is never used without a content identifier.
-            if (empty($this->other['content'])) {
-                throw new \coding_exception('content_viewed event must define content identifier.');
-            }
+        parent::validate_data();
+        // Make sure this class is never used without a content identifier.
+        if (empty($this->other['content'])) {
+            throw new \coding_exception('content_viewed event must define content identifier.');
         }
     }
 }
index b049a8a..c131f27 100644 (file)
@@ -45,6 +45,15 @@ class course_category_created extends base {
         return get_string('eventcoursecategorycreated');
     }
 
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/management.php', array('categoryid' => $this->objectid));
+    }
+
     /**
      * Returns non-localised description of what happened.
      *
index 1a0055f..2cb7739 100644 (file)
@@ -48,6 +48,15 @@ class course_category_updated extends base {
         return get_string('eventcoursecategoryupdated');
     }
 
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/editcategory.php', array('id' => $this->objectid));
+    }
+
     /**
      * Returns non-localised description of what happened.
      *
index 53897cb..6233bfb 100644 (file)
@@ -87,6 +87,7 @@ class course_module_completion_updated extends base {
      * @throws \coding_exception in case of a problem.
      */
     protected function validate_data() {
+        parent::validate_data();
         // Make sure the context level is set to module.
         if ($this->contextlevel !== CONTEXT_MODULE) {
             throw new \coding_exception('Context passed must be module context.');
index e4b7b3d..474caee 100644 (file)
@@ -120,6 +120,7 @@ class course_module_created extends base {
      * Throw \coding_exception notice in case of any problems.
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['modulename'])) {
             throw new \coding_exception("Field other['modulename'] cannot be empty");
         }
index 4c58a3d..82d01d1 100644 (file)
@@ -110,6 +110,7 @@ class course_module_deleted extends base {
      * Throw \coding_exception notice in case of any problems.
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['modulename'])) {
             throw new \coding_exception("Field other['modulename'] cannot be empty");
         }
index f33bde5..87c222a 100644 (file)
@@ -42,8 +42,8 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class course_module_instance_list_viewed extends base{
 
-    /** @var string private var to store mod name */
-    private $modname;
+    /** @var string protected var to store mod name */
+    protected $modname;
 
     /**
      * Init method.
@@ -105,6 +105,7 @@ abstract class course_module_instance_list_viewed extends base{
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if ($this->contextlevel != CONTEXT_COURSE) {
             throw new \coding_exception('Context passed must be course context.');
         }
index 9ac5594..6ae6752 100644 (file)
@@ -120,6 +120,7 @@ class course_module_updated extends base {
      * Throw \coding_exception notice in case of any problems.
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['modulename'])) {
             throw new \coding_exception("Field other['modulename'] cannot be empty");
         }
index a6461ab..9f9b654 100644 (file)
@@ -92,6 +92,7 @@ abstract class course_module_viewed extends base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         // Make sure this class is never used without proper object details.
         if (empty($this->objectid) || empty($this->objecttable)) {
             throw new \coding_exception('course_module_viewed event must define objectid and object table.');
index 1b8fdb8..02aa67c 100644 (file)
@@ -84,6 +84,7 @@ class course_reset_ended extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['reset_options'])) {
            throw new \coding_exception('The key reset_options must be set in $other.');
         }
index 8a2be3c..91531ef 100644 (file)
@@ -84,6 +84,7 @@ class course_reset_started extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['reset_options'])) {
            throw new \coding_exception('The key reset_options must be set in $other.');
         }
index 1ca5407..a7d8bc3 100644 (file)
@@ -70,7 +70,7 @@ class course_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/course/view.php', array('id' => $this->objectid));
+        return new \moodle_url('/course/edit.php', array('id' => $this->objectid));
     }
 
     /**
index 40affbd..d0220da 100644 (file)
@@ -69,6 +69,7 @@ class email_failed extends base {
      * @throws \coding_exception
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['subject'])) {
             throw new \coding_exception('The subject needs to be set in $other');
         }
index 2c1cf28..a248f91 100644 (file)
@@ -53,7 +53,7 @@ class group_member_added extends \core\event\base {
     /**
      * Legacy event data if get_legacy_eventname() is not empty.
      *
-     * @return stdClass
+     * @return \stdClass
      */
     protected function get_legacy_eventdata() {
         $eventdata = new \stdClass();
@@ -88,7 +88,7 @@ class group_member_added extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/group/index.php', array('id' => $this->courseid));
+        return new \moodle_url('/group/members.php', array('group' => $this->objectid));
     }
 
     /**
@@ -112,6 +112,7 @@ class group_member_added extends \core\event\base {
         if (!isset($this->other['component']) || !isset($this->other['itemid'])) {
             throw new \coding_exception('The component and itemid need to be set in $other, even if empty.');
         }
+        parent::validate_data();
     }
 
 }
index cc5fd26..8557359 100644 (file)
@@ -46,7 +46,7 @@ class group_member_removed extends \core\event\base {
     /**
      * Legacy event data if get_legacy_eventname() is not empty.
      *
-     * @return stdClass
+     * @return \stdClass
      */
     protected function get_legacy_eventdata() {
         $eventdata = new \stdClass();
@@ -79,7 +79,7 @@ class group_member_removed extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/group/index.php', array('id' => $this->courseid));
+        return new \moodle_url('/group/members.php', array('group' => $this->objectid));
     }
 
     /**
index 8631ca4..4b03799 100644 (file)
@@ -76,7 +76,7 @@ class group_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/group/index.php', array('id' => $this->courseid));
+        return new \moodle_url('/group/group.php', array('id' => $this->objectid));
     }
 
     /**
index 2f79a6a..b4ecb1a 100644 (file)
@@ -76,7 +76,7 @@ class grouping_created extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/group/groupings/index.php', array('id' => $this->courseid));
+        return new \moodle_url('/group/groupings.php', array('id' => $this->courseid));
     }
 
     /**
index 0176cc9..3603be9 100644 (file)
@@ -83,7 +83,7 @@ class grouping_deleted extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/group/groupings/index.php', array('id' => $this->courseid));
+        return new \moodle_url('/group/groupings.php', array('id' => $this->courseid));
     }
 
     /**
index 5e81352..4e351dd 100644 (file)
@@ -76,7 +76,7 @@ class grouping_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/group/groupings/index.php', array('id' => $this->courseid));
+        return new \moodle_url('/group/grouping.php', array('id' => $this->objectid));
     }
 
     /**
index 7281048..6ea7dd8 100644 (file)
@@ -51,7 +51,7 @@ class message_contact_added extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('message/index.php', array('user1' => $this->relateduserid, 'user2' => $this->userid));
+        return new \moodle_url('/message/index.php', array('user1' => $this->userid, 'user2' => $this->relateduserid));
     }
 
     /**
index 7ea6bd4..3d79fbd 100644 (file)
@@ -51,7 +51,7 @@ class message_contact_blocked extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('message/index.php', array('user1' => $this->relateduserid, 'user2' => $this->userid));
+        return new \moodle_url('/message/index.php', array('user1' => $this->userid, 'user2' => $this->relateduserid));
     }
 
     /**
index 40761f6..78c0d04 100644 (file)
@@ -51,7 +51,7 @@ class message_contact_removed extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('message/index.php', array('user1' => $this->relateduserid, 'user2' => $this->userid));
+        return new \moodle_url('/message/index.php', array('user1' => $this->userid, 'user2' => $this->relateduserid));
     }
 
     /**
index cca7464..2475511 100644 (file)
@@ -51,7 +51,7 @@ class message_contact_unblocked extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('message/index.php', array('user1' => $this->relateduserid, 'user2' => $this->userid));
+        return new \moodle_url('/message/index.php', array('user1' => $this->userid, 'user2' => $this->relateduserid));
     }
 
     /**
index 6ba652e..664f2f2 100644 (file)
@@ -57,7 +57,7 @@ class message_read extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('message/index.php', array('user1' => $this->relateduserid, 'user2' => $this->userid));
+        return new \moodle_url('/message/index.php', array('user1' => $this->userid, 'user2' => $this->relateduserid));
     }
 
     /**
index 453f74a..3aad156 100644 (file)
@@ -56,7 +56,7 @@ class message_sent extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('message/index.php', array('user1' => $this->relateduserid, 'user2' => $this->userid));
+        return new \moodle_url('/message/index.php', array('user1' => $this->userid, 'user2' => $this->relateduserid));
     }
 
     /**
index 3bf5979..cf9cb13 100644 (file)
@@ -51,7 +51,7 @@ class mnet_access_control_created extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('admin/mnet/access_control.php');
+        return new \moodle_url('/admin/mnet/access_control.php');
     }
 
     /**
@@ -60,11 +60,8 @@ class mnet_access_control_created extends base {
      * @return string
      */
     public function get_description() {
-        $mnetaccesscontrol = $this->get_record_snapshot('mnet_sso_access_control', $this->objectid);
-        $mnethost = $this->get_record_snapshot('mnet_host', $mnetaccesscontrol->mnet_host_id);
-
-        return 'Access control created for the user with the username \'' . $mnetaccesscontrol->username . '\' belonging
-            to the mnet host \'' . $mnethost->name . '\'';
+        return 'Access control created for the user with the username \'' . $this->other['username'] . '\' belonging
+            to the mnet host \'' . $this->other['hostname'] . '\'';
     }
 
     /**
@@ -73,11 +70,29 @@ class mnet_access_control_created extends base {
      * @return array
      */
     protected function get_legacy_logdata() {
-        $mnetaccesscontrol = $this->get_record_snapshot('mnet_sso_access_control', $this->objectid);
-        $mnethost = $this->get_record_snapshot('mnet_host', $mnetaccesscontrol->mnet_host_id);
+        return array(SITEID, 'admin/mnet', 'add', 'admin/mnet/access_control.php', 'SSO ACL: ' . $this->other['accessctrl'] .
+            ' user \'' . $this->other['username'] . '\' from ' . $this->other['hostname']);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['username'])) {
+            throw new \coding_exception('The \'username\' must be set in other.');
+        }
+
+        if (!isset($this->other['hostname'])) {
+            throw new \coding_exception('The \'hostname\' must be set in other.');
+        }
 
-        return array(SITEID, 'admin/mnet', 'add', 'admin/mnet/access_control.php', 'SSO ACL: ' .
-            $mnetaccesscontrol->accessctrl . ' user \'' . $mnetaccesscontrol->username . '\' from ' .
-            $mnethost->name);
+        if (!isset($this->other['accessctrl'])) {
+            throw new \coding_exception('The \'accessctrl\' must be set in other.');
+        }
     }
 }
index 88e309d..79ac0ec 100644 (file)
@@ -51,7 +51,7 @@ class mnet_access_control_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('admin/mnet/access_control.php');
+        return new \moodle_url('/admin/mnet/access_control.php');
     }
 
     /**
@@ -60,11 +60,8 @@ class mnet_access_control_updated extends base {
      * @return string
      */
     public function get_description() {
-        $mnetaccesscontrol = $this->get_record_snapshot('mnet_sso_access_control', $this->objectid);
-        $mnethost = $this->get_record_snapshot('mnet_host', $mnetaccesscontrol->mnet_host_id);
-
-        return 'Access control created for the user with the username \'' . $mnetaccesscontrol->username . '\' belonging
-            to the mnet host \'' . $mnethost->name . '\'';
+        return 'Access control created for the user with the username \'' . $this->other['username'] . '\' belonging
+            to the mnet host \'' . $this->other['hostname'] . '\'';
     }
 
     /**
@@ -73,11 +70,29 @@ class mnet_access_control_updated extends base {
      * @return array
      */
     protected function get_legacy_logdata() {
-        $mnetaccesscontrol = $this->get_record_snapshot('mnet_sso_access_control', $this->objectid);
-        $mnethost = $this->get_record_snapshot('mnet_host', $mnetaccesscontrol->mnet_host_id);
+        return array(SITEID, 'admin/mnet', 'update', 'admin/mnet/access_control.php', 'SSO ACL: ' . $this->other['accessctrl'] .
+            ' user \'' . $this->other['username'] . '\' from ' . $this->other['hostname']);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['username'])) {
+            throw new \coding_exception('The \'username\' must be set in other.');
+        }
+
+        if (!isset($this->other['hostname'])) {
+            throw new \coding_exception('The \'hostname\' must be set in other.');
+        }
 
-        return array(SITEID, 'admin/mnet', 'update', 'admin/mnet/access_control.php', 'SSO ACL: ' .
-            $mnetaccesscontrol->accessctrl . ' user \'' . $mnetaccesscontrol->username . '\' from ' .
-            $mnethost->name);
+        if (!isset($this->other['accessctrl'])) {
+            throw new \coding_exception('The \'accessctrl\' must be set in other.');
+        }
     }
 }
index 9431cd3..9347479 100644 (file)
@@ -64,7 +64,7 @@ class role_capabilities_updated extends base {
      */
     public function get_url() {
         if ($this->contextlevel === CONTEXT_SYSTEM) {
-            return new \moodle_url('admin/roles/define.php', array('action' => 'view', 'roleid' => $this->objectid));
+            return new \moodle_url('/admin/roles/define.php', array('action' => 'view', 'roleid' => $this->objectid));
         } else {
             return new \moodle_url('/admin/roles/override.php', array('contextid' => $this->contextid,
                 'roleid' => $this->objectid));
index 1637822..37d9edb 100644 (file)
@@ -31,11 +31,11 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      @type string username name of user.
- *      @type string email user email.
- *      @type string idnumber user idnumber.
- *      @type string picture user picture.
- *      @type int mnethostid mnet host id.
+ *      - string username: name of user.
+ *      - string email: user email.
+ *      - string idnumber: user idnumber.
+ *      - string picture: user picture.
+ *      - int mnethostid: mnet host id.
  * }
  *
  * @package    core
@@ -68,8 +68,7 @@ class user_deleted extends base {
      * @return string
      */
     public function get_description() {
-        $user = $this->get_record_snapshot('user', $this->data['objectid']);
-        return 'User profile deleted for userid ' . $user->id;
+        return 'User profile deleted for the user with the id ' . $this->objectid;
     }
 
     /**
@@ -87,12 +86,13 @@ class user_deleted extends base {
      * @return \stdClass user data.
      */
     protected function get_legacy_eventdata() {
-        $user = $this->get_record_snapshot('user', $this->data['objectid']);
+        $user = $this->get_record_snapshot('user', $this->objectid);
         $user->deleted = 0;
-        $user->username = $this->data['other']['username'];
-        $user->email = $this->data['other']['email'];
-        $user->idnumber = $this->data['other']['idnumber'];
-        $user->picture = $this->data['other']['picture'];
+        $user->username = $this->other['username'];
+        $user->email = $this->other['email'];
+        $user->idnumber = $this->other['idnumber'];
+        $user->picture = $this->other['picture'];
+
         return $user;
     }
 
@@ -102,8 +102,8 @@ class user_deleted extends base {
      * @return array
      */
     protected function get_legacy_logdata() {
-        $user = $this->get_record_snapshot('user', $this->data['objectid']);
-        return array(SITEID, 'user', 'delete', "view.php?id=".$user->id, $user->firstname.' '.$user->lastname);
+        $user = $this->get_record_snapshot('user', $this->objectid);
+        return array(SITEID, 'user', 'delete', 'view.php?id=' . $user->id, $user->firstname . ' ' . $user->lastname);
     }
 
     /**
@@ -113,29 +113,26 @@ class user_deleted extends base {
      * @return void
      */
     protected function validate_data() {
-        global $CFG;
+        parent::validate_data();
 
-        if ($CFG->debugdeveloper) {
-            parent::validate_data();
-            if (!isset($this->other['username'])) {
-                throw new \coding_exception('username must be set in $other.');
-            }
+        if (!isset($this->other['username'])) {
+            throw new \coding_exception('username must be set in $other.');
+        }
 
-            if (!isset($this->other['email'])) {
-                throw new \coding_exception('email must be set in $other.');
-            }
+        if (!isset($this->other['email'])) {
+            throw new \coding_exception('email must be set in $other.');
+        }
 
-            if (!isset($this->other['idnumber'])) {
-                throw new \coding_exception('idnumber must be set in $other.');
-            }
+        if (!isset($this->other['idnumber'])) {
+            throw new \coding_exception('idnumber must be set in $other.');
+        }
 
-            if (!isset($this->other['picture'])) {
-                throw new \coding_exception('picture must be set in $other.');
-            }
+        if (!isset($this->other['picture'])) {
+            throw new \coding_exception('picture must be set in $other.');
+        }
 
-            if (!isset($this->other['mnethostid'])) {
-                throw new \coding_exception('mnethostid must be set in $other.');
-            }
+        if (!isset($this->other['mnethostid'])) {
+            throw new \coding_exception('mnethostid must be set in $other.');
         }
     }
 }
index 5ae4c03..5bfad22 100644 (file)
@@ -73,7 +73,7 @@ class user_enrolment_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/enrol/users.php', array('id' => $this->courseid));
+        return new \moodle_url('/enrol/editenrolment.php', array('ue' => $this->objectid));
     }
 
     /**
diff --git a/lib/classes/event/user_graded.php b/lib/classes/event/user_graded.php
new file mode 100644 (file)
index 0000000..ae77611
--- /dev/null
@@ -0,0 +1,135 @@
+<?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/>.
+
+/**
+ * Grade edited event.
+ *
+ * @package    core_grades
+ * @copyright  2014 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after teacher edits manual grade or
+ * overrides activity/aggregated grade.
+ *
+ * Note: use grade_grades_history table if you need to know
+ *       the history of grades.
+ *
+ * @property-read array $other Extra information about the event.
+ *     -int itemid: grade item id.
+ *     -bool overridden: Is this grade override?
+ *     -float finalgrade: the final grade value.
+ *
+ * @package    core_grades
+ * @copyright  2013 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_graded extends \core\event\base {
+    /** @var \grade_grade $grade */
+    protected $grade;
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param \grade_grade $grade
+     * @return user_graded
+     */
+    public static function create_from_grade(\grade_grade $grade) {
+        $event = self::create(array(
+            'context'       => \context_course::instance($grade->grade_item->courseid),
+            'objectid'      => $grade->id,
+            'relateduserid' => $grade->userid,
+            'other'         => array(
+                'itemid'     => $grade->itemid,
+                'overridden' => !empty($grade->overridden),
+                'finalgrade' => $grade->finalgrade),
+        ));
+        $event->grade = $grade;
+        return $event;
+    }
+
+    /**
+     * Get grade object.
+     *
+     * @return \grade_grade
+     */
+    public function get_grade() {
+        if ($this->is_restored()) {
+            throw new \coding_exception('get_grade() is intended for event observers only');
+        }
+        return $this->grade;
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+        $this->data['objecttable'] = 'grade_grades';
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventusergraded', 'core_grades');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "User {$this->userid} edited grade of user {$this->objectid} for grade item " . $this->other['itemid'];
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/grade/edit/tree/grade.php', array(
+            'courseid' => $this->courseid,
+            'itemid'   => $this->other['itemid'],
+            'userid'   => $this->relateduserid,
+        ));
+    }
+
+    /**
+     * Return legacy log info.
+     *
+     * @return null|array of parameters to be passed to legacy add_to_log() function.
+     */
+    public function get_legacy_logdata() {
+        $user = $this->get_record_snapshot('user', $this->relateduserid);
+        $fullname = fullname($user);
+        $info = $this->grade->grade_item->itemname . ': ' . $fullname;
+        $url = '/report/grader/index.php?id=' . $this->courseid;
+
+        return array($this->courseid, 'grade', 'update', $url, $info);
+    }
+}
index 198a137..ec94f35 100644 (file)
@@ -106,11 +106,11 @@ class user_loggedin extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->data['objectid'])) {
             throw new \coding_exception("objectid has to be specified.");
         } else if (!isset($this->data['other']['username'])) {
             throw new \coding_exception("other['username'] has to be specified.");
         }
     }
-
 }
index 6533a10..7ea66cf 100644 (file)
@@ -99,6 +99,7 @@ class user_login_failed extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['reason'])) {
             throw new \coding_exception("other['reason'] has to be specified.");
         } else if (!isset($this->other['username'])) {
index 7685abd..a2f3e8c 100644 (file)
@@ -99,6 +99,7 @@ class webservice_function_called extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['function'])) {
            throw new \coding_exception('The key \'function\' needs to be set in $other.');
         }
index 78560d3..7d82a95 100644 (file)
@@ -113,6 +113,7 @@ class webservice_login_failed extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->other['reason'])) {
            throw new \coding_exception('The key \'reason\' needs to be set in $other.');
         } else if (!isset($this->other['method'])) {
index 9740329..121f7a3 100644 (file)
@@ -70,7 +70,7 @@ class webservice_service_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/admin/settings.php', array('section' => 'externalservices'));
+        return new \moodle_url('/admin/webservice/service.php', array('id' => $this->objectid));
     }
 
     /**
index c9c4d6d..4bf7c94 100644 (file)
@@ -90,6 +90,7 @@ class webservice_service_user_added extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->relateduserid)) {
             throw new \coding_exception('The relateduserid must be set.');
         }
index c3770a8..7114b47 100644 (file)
@@ -90,6 +90,7 @@ class webservice_service_user_removed extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->relateduserid)) {
             throw new \coding_exception('The relateduserid must be set.');
         }
index cb0c653..db241df 100644 (file)
@@ -98,6 +98,7 @@ class webservice_token_created extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
+        parent::validate_data();
         if (!isset($this->relateduserid)) {
            throw new \coding_exception('The property \'relateduserid\' must be set.');
         }
index ea573c9..be60ee2 100644 (file)
@@ -198,6 +198,7 @@ class manager {
         $record->day = $task->get_day();
         $record->dayofweek = $task->get_day_of_week();
         $record->month = $task->get_month();
+        $record->disabled = $task->get_disabled();
 
         return $record;
     }
@@ -306,6 +307,9 @@ class manager {
         if (isset($record->faildelay)) {
             $task->set_fail_delay($record->faildelay);
         }
+        if (isset($record->disabled)) {
+            $task->set_disabled($record->disabled);
+        }
 
         return $task;
     }
@@ -446,7 +450,9 @@ class manager {
             throw new \moodle_exception('locktimeout');
         }
 
-        $where = '(lastruntime IS NULL OR lastruntime < :timestart1) AND (nextruntime IS NULL OR nextruntime < :timestart2)';
+        $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
+                  AND (nextruntime IS NULL OR nextruntime < :timestart2)
+                  AND disabled = 0";
         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
         $records = $DB->get_records_select('task_scheduled', $where, $params);
 
index 1c36b07..f7fb050 100644 (file)
@@ -52,6 +52,9 @@ abstract class scheduled_task extends task_base {
     /** @var boolean $customised - Has this task been changed from it's default schedule? */
     private $customised = false;
 
+    /** @var int $disabled - Is this task disabled in cron? */
+    private $disabled = false;
+
     /**
      * Get the last run time for this scheduled task.
      * @return int
@@ -164,6 +167,22 @@ abstract class scheduled_task extends task_base {
         return $this->dayofweek;
     }
 
+    /**
+     * Setter for $disabled.
+     * @param bool $disabled
+     */
+    public function set_disabled($disabled) {
+        $this->disabled = (bool)$disabled;
+    }
+
+    /**
+     * Getter for $disabled.
+     * @return bool
+     */
+    public function get_disabled() {
+        return $this->disabled;
+    }
+
     /**
      * Take a cron field definition and return an array of valid numbers with the range min-max.
      *
index 1c71b6f..7791fcf 100644 (file)
@@ -42,7 +42,7 @@ class send_failed_login_notifications_task extends scheduled_task {
      * Throw exceptions on errors (the job will be retried).
      */
     public function execute() {
-        global $CFG, $DB, $OUTPUT;
+        global $CFG, $DB;
 
         if (empty($CFG->notifyloginfailures)) {
             return;
@@ -66,13 +66,23 @@ class send_failed_login_notifications_task extends scheduled_task {
 
         // Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure
         // and insert them into the cache_flags temp table.
+        $logmang = get_log_manager();
+        $readers = $logmang->get_readers('\core\log\sql_internal_reader');
+        $reader = reset($readers);
+        $readername = key($readers);
+        if (empty($reader) || empty($readername)) {
+            // No readers, no processing.
+            return true;
+        }
+        $logtable = $reader->get_internal_log_table_name();
+
         $sql = "SELECT ip, COUNT(*)
-                  FROM {log}
-                 WHERE module = 'login' AND action = 'error'
-                       AND time > ?
-              GROUP BY ip
-                HAVING COUNT(*) >= ?";
-        $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
+                  FROM {" . $logtable . "}
+                 WHERE eventname = ?
+                       AND timecreated > ?
+               GROUP BY ip
+                 HAVING COUNT(*) >= ?";
+        $params = array('\core\event\user_login_failed', $CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
         $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $iprec) {
             if (!empty($iprec->ip)) {
@@ -83,17 +93,17 @@ class send_failed_login_notifications_task extends scheduled_task {
 
         // Get all the INFOs with more than notifyloginthreshold failures since lastnotifyfailure
         // and insert them into the cache_flags temp table.
-        $sql = "SELECT info, count(*)
-                  FROM {log}
-                 WHERE module = 'login' AND action = 'error'
-                       AND time > ?
-              GROUP BY info
+        $sql = "SELECT userid, count(*)
+                  FROM {" . $logtable . "}
+                 WHERE eventname = ?
+                       AND timecreated > ?
+              GROUP BY userid
                 HAVING count(*) >= ?";
-        $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
+        $params = array('\core\event\user_login_failed', $CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
         $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $inforec) {
             if (!empty($inforec->info)) {
-                set_cache_flag('login_failure_by_info', $inforec->info, '1', 0);
+                set_cache_flag('login_failure_by_id', $inforec->userid, '1', 0);
             }
         }
         $rs->close();
@@ -101,23 +111,23 @@ class send_failed_login_notifications_task extends scheduled_task {
         // 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 * 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
-                 WHERE l.module = 'login' AND l.action = 'error'
-                       AND l.time > ?
-                       AND cf.flagtype = 'login_failure_by_ip'
-            UNION ALL
-                SELECT l.*, u.firstname, u.lastname
-                  FROM {log} l
-                  JOIN {cache_flags} cf ON l.info = cf.name
-             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') t
-            ORDER BY t.time DESC";
-        $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure);
+                        SELECT l.*, u.username
+                          FROM {" . $logtable . "} l
+                          JOIN {cache_flags} cf ON l.ip = cf.name
+                     LEFT JOIN {user} u         ON l.userid = u.id
+                         WHERE l.eventname = ?
+                               AND l.timecreated > ?
+                               AND cf.flagtype = 'login_failure_by_ip'
+                    UNION ALL
+                        SELECT l.*, u.username
+                          FROM {" . $logtable . "} l
+                          JOIN {cache_flags} cf ON l.userid = " . $DB->sql_cast_char2int('cf.name') . "
+                     LEFT JOIN {user} u         ON l.userid = u.id
+                         WHERE l.eventname = ?
+                               AND l.timecreated > ?
+                               AND cf.flagtype = 'login_failure_by_info') t
+             ORDER BY t.timecreated DESC";
+        $params = array('\core\event\user_login_failed', $CFG->lastnotifyfailure, '\core\event\user_login_failed', $CFG->lastnotifyfailure);
 
         // Init some variables.
         $count = 0;
@@ -125,8 +135,17 @@ class send_failed_login_notifications_task extends scheduled_task {
         // Iterate over the logs recordset.
         $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $log) {
-            $log->time = userdate($log->time);
-            $messages .= get_string('notifyloginfailuresmessage', '', $log) . "\n";
+            $a = new \stdClass();
+            $a->time = userdate($log->timecreated);
+            if (empty($log->username)) {
+                // Entries with no valid username. We get attempted username from the event's other field.
+                $other = unserialize($log->other);
+                $a->info = empty($other['username']) ? '' : $other['username'];
+            } else {
+                $a->info = $log->username;
+            }
+            $a->ip = $log->ip;
+            $messages .= get_string('notifyloginfailuresmessage', '', $a)."\n";
             $count++;
         }
         $rs->close();
@@ -136,10 +155,12 @@ class send_failed_login_notifications_task extends scheduled_task {
             $site = get_site();
             $subject = get_string('notifyloginfailuressubject', '', format_string($site->fullname));
             // Calculate the complete body of notification (start + messages + end).
+            $params = array('id' => 0, 'modid' => 'site_errors', 'chooselog' => '1', 'logreader' => $readername);
+            $url = new \moodle_url('/report/log/index.php', $params);
             $body = get_string('notifyloginfailuresmessagestart', '', $CFG->wwwroot) .
                     (($CFG->lastnotifyfailure != 0) ? '('.userdate($CFG->lastnotifyfailure).')' : '')."\n\n" .
                     $messages .
-                    "\n\n" . get_string('notifyloginfailuresmessageend', '', $CFG->wwwroot) . "\n\n";
+                    "\n\n".get_string('notifyloginfailuresmessageend', '',  $url->out(false).' ')."\n\n";
 
             // For each destination, send mail.
             mtrace('Emailing admins about '. $count .' failed login attempts');
index 2faabe6..05e68a5 100644 (file)
@@ -75,8 +75,8 @@ abstract class task_base {
     }
 
     /**
-     * Get the last run time for this task.
-     * @return int
+     * Get the next run time for this task.
+     * @return int timestamp
      */
     public function get_next_run_time() {
         return $this->nextruntime;
index 7bcee34..bad53f5 100644 (file)
@@ -79,6 +79,31 @@ class core_user {
         }
     }
 
+
+    /**
+     * Return user object from db based on their username.
+     *
+     * @param string $username The username of the user searched.
+     * @param string $fields A comma separated list of user fields to be returned, support and noreply user.
+     * @param int $mnethostid The id of the remote host.
+     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
+     *                        IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
+     *                        MUST_EXIST means throw an exception if no user record or multiple records found.
+     * @return stdClass|bool user record if found, else false.
+     * @throws dml_exception if user record not found and respective $strictness is set.
+     */
+    public static function get_user_by_username($username, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
+        global $DB, $CFG;
+
+        // Because we use the username as the search criteria, we must also restrict our search based on mnet host.
+        if (empty($mnethostid)) {
+            // If empty, we restrict to local users.
+            $mnethostid = $CFG->mnet_localhost_id;
+        }
+
+        return $DB->get_record('user', array('username' => $username, 'mnethostid' => $mnethostid), $fields, $strictness);
+    }
+
     /**
      * Helper function to return dummy noreply user record.
      *
index 9e7cd61..8c45183 100644 (file)
@@ -1832,43 +1832,6 @@ function get_logs_userday($userid, $courseid, $daystart) {
                                GROUP BY FLOOR((time - $daystart)/". HOURSECS .") ", $params);
 }
 
-/**
- * Returns an object with counts of failed login attempts
- *
- * Returns information about failed login attempts.  If the current user is
- * an admin, then two numbers are returned:  the number of attempts and the
- * number of accounts.  For non-admins, only the attempts on the given user
- * are shown.
- *
- * @global moodle_database $DB
- * @uses CONTEXT_SYSTEM
- * @param string $mode Either 'admin' or 'everybody'
- * @param string $username The username we are searching for
- * @param string $lastlogin The date from which we are searching
- * @return int
- */
-function count_login_failures($mode, $username, $lastlogin) {
-    global $DB;
-
-    $params = array('mode'=>$mode, 'username'=>$username, 'lastlogin'=>$lastlogin);
-    $select = "module='login' AND action='error' AND time > :lastlogin";
-
-    $count = new stdClass();
-
-    if (is_siteadmin()) {
-        if ($count->attempts = $DB->count_records_select('log', $select, $params)) {
-            $count->accounts = $DB->count_records_select('log', $select, $params, 'COUNT(DISTINCT info)');
-            return $count;
-        }
-    } else if ($mode == 'everybody') {
-        if ($count->attempts = $DB->count_records_select('log', "$select AND info = :username", $params)) {
-            return $count;
-        }
-    }
-    return NULL;
-}
-
-
 /// GENERAL HELPFUL THINGS  ///////////////////////////////////
 
 /**
index 5f18624..3627e91 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20140213" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20140324" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
       </FIELDS>
       <KEYS>
-          <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
     <TABLE NAME="question_response_count" COMMENT="Count for each responses for each try at a question.">
         <FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
-          <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-          <KEY NAME="analysisid" TYPE="foreign" FIELDS="analysisid" REFTABLE="question_response_analysis" REFFIELDS="id"/>
-        </KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="analysisid" TYPE="foreign" FIELDS="analysisid" REFTABLE="question_response_analysis" REFFIELDS="id"/>
+      </KEYS>
     </TABLE>
     <TABLE NAME="mnet_application" COMMENT="Information about applications on remote hosts">
       <FIELDS>
         <FIELD NAME="dayofweek" TYPE="char" LENGTH="25" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="faildelay" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="customised" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used on upgrades to prevent overwriting custom schedules."/>
+        <FIELD NAME="disabled" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="1 means do not run from cron"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 2ce903c..298cb26 100644 (file)
@@ -3085,6 +3085,7 @@ function xmldb_main_upgrade($oldversion) {
         $table->add_field('dayofweek', XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, null);
         $table->add_field('faildelay', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
         $table->add_field('customised', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('disabled', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
 
         // Adding keys to table task_scheduled.
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
@@ -3331,5 +3332,33 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014032600.02);
     }
 
+    if ($oldversion < 2014032700.01) {
+
+        // Define field disabled to be added to task_scheduled.
+        $table = new xmldb_table('task_scheduled');
+        $field = new xmldb_field('disabled', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'customised');
+
+        // Conditionally launch add field disabled.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014032700.01);
+    }
+
+    if ($oldversion < 2014032700.02) {
+
+        // Update displayloginfailures setting.
+        if (empty($CFG->displayloginfailures)) {
+            set_config('displayloginfailures', 0);
+        } else {
+            set_config('displayloginfailures', 1);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014032700.02);
+    }
+
     return true;
 }
index c4d29a9..2d81c77 100644 (file)
@@ -4335,3 +4335,57 @@ function can_use_html_editor() {
     debugging('can_use_html_editor has been deprecated please update your code to assume it returns true.', DEBUG_DEVELOPER);
     return true;
 }
+
+
+/**
+ * Returns an object with counts of failed login attempts
+ *
+ * Returns information about failed login attempts.  If the current user is
+ * an admin, then two numbers are returned:  the number of attempts and the
+ * number of accounts.  For non-admins, only the attempts on the given user
+ * are shown.
+ *
+ * @deprecate since Moodle 2.7, use {@link user_count_login_failures()} instead.
+ * @global moodle_database $DB
+ * @uses CONTEXT_SYSTEM
+ * @param string $mode Either 'admin' or 'everybody'
+ * @param string $username The username we are searching for
+ * @param string $lastlogin The date from which we are searching
+ * @return int
+ */
+function count_login_failures($mode, $username, $lastlogin) {
+    global $DB;
+
+    debugging('This method has been deprecated. Please use user_count_login_failures() instead.', DEBUG_DEVELOPER);
+
+    $params = array('mode'=>$mode, 'username'=>$username, 'lastlogin'=>$lastlogin);
+    $select = "module='login' AND action='error' AND time > :lastlogin";
+
+    $count = new stdClass();
+
+    if (is_siteadmin()) {
+        if ($count->attempts = $DB->count_records_select('log', $select, $params)) {
+            $count->accounts = $DB->count_records_select('log', $select, $params, 'COUNT(DISTINCT info)');
+            return $count;
+        }
+    } else if ($mode == 'everybody') {
+        if ($count->attempts = $DB->count_records_select('log', "$select AND info = :username", $params)) {
+            return $count;
+        }
+    }
+    return NULL;
+}
+
+/**
+ * Returns whether ajax is enabled/allowed or not.
+ * This function is deprecated and always returns true.
+ *
+ * @param array $unused - not used any more.
+ * @return bool
+ * @deprecated since 2.7 MDL-33099 - please do not use this function any more.
+ * @todo MDL-44088 This will be removed in Moodle 2.9.
+ */
+function ajaxenabled(array $browsers = null) {
+    debugging('ajaxenabled() is deprecated - please update your code to assume it returns true.', DEBUG_DEVELOPER);
+    return true;
+}
index 717e632..1b8150c 100644 (file)
@@ -136,6 +136,10 @@ class pgsql_native_moodle_database extends moodle_database {
             $connection = "user='$this->dbuser' password='$pass' dbname='$this->dbname'";
             if (strpos($this->dboptions['dbsocket'], '/') !== false) {
                 $connection = $connection." host='".$this->dboptions['dbsocket']."'";
+                if (!empty($this->dboptions['dbport'])) {
+                    // Somehow non-standard port is important for sockets - see MDL-44862.
+                    $connection = $connection." port ='".$this->dboptions['dbport']."'";
+                }
             }
         } else {
             $this->dboptions['dbsocket'] = '';
index 8c5a623..a1b9bd7 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Admin setting for toolbar.
+ *
+ * @package    editor_atto
+ * @copyright  2014 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editor_atto_toolbar_setting extends admin_setting_configtextarea {
+
+    /**
+     * Validate data.
+     *
+     * This ensures that:
+     * - Plugins are only used once,
+     * - Group names are unique,
+     * - Lines match: group = plugin[, plugin[, plugin ...]],
+     * - There are some groups and plugins defined,
+     * - The plugins used are installed.
+     *
+     * @param string $data
+     * @return mixed True on success, else error message.
+     */
+    public function validate($data) {
+        $result = parent::validate($data);
+        if ($result !== true) {
+            return $result;
+        }
+
+        $lines = explode("\n", $data);
+        $groups = array();
+        $plugins = array();
+
+        foreach ($lines as $line) {
+            if (!trim($line)) {
+                continue;
+            }
+
+            $matches = array();
+            if (!preg_match('/^\s*([a-z0-9]+)\s*=\s*([a-z0-9]+(\s*,\s*[a-z0-9]+)*)+\s*$/', $line, $matches)) {
+                $result = get_string('errorcannotparseline', 'editor_atto', $line);
+                break;
+            }
+
+            $group = $matches[1];
+            if (isset($groups[$group])) {
+                $result = get_string('errorgroupisusedtwice', 'editor_atto', $group);
+                break;
+            }
+            $groups[$group] = true;
+
+            $lineplugins = array_map('trim', explode(',', $matches[2]));
+            foreach ($lineplugins as $plugin) {
+                if (isset($plugins[$plugin])) {
+                    $result = get_string('errorpluginisusedtwice', 'editor_atto', $plugin);
+                    break 2;
+                } else if (!core_component::get_component_directory('atto_' . $plugin)) {
+                    $result = get_string('errorpluginnotfound', 'editor_atto', $plugin);
+                    break 2;
+                }
+                $plugins[$plugin] = true;
+            }
+        }
+
+        // We did not find any groups or plugins.
+        if (empty($groups) || empty($plugins)) {
+            $result = get_string('errornopluginsorgroupsfound', 'editor_atto');
+        }
+
+        return $result;
+    }
+
+}
 
 /**
  * Special class for Atto plugins administration.
@@ -32,7 +104,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2014 Jerome Mouneyrac
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class atto_subplugins_settings extends admin_setting {
+class editor_atto_subplugins_setting extends admin_setting {
 
     /**
      * Constructor.
@@ -130,44 +202,6 @@ class atto_subplugins_settings extends admin_setting {
         $table->data  = array();
         $table->attributes['class'] = 'admintable generaltable';
 
-        $corepluginicons = array(
-            'accessibilitychecker' => $OUTPUT->pix_url('e/visual_blocks', 'core'),
-            'accessibilityhelper' => $OUTPUT->pix_url('e/visual_aid'),
-            'align' => array(
-                $OUTPUT->pix_url('e/align_left', 'core'),
-                $OUTPUT->pix_url('e/align_center', 'core'), $OUTPUT->pix_url('e/align_right', 'core')
-            ),
-            'backcolor' => $OUTPUT->pix_url('e/text_highlight', 'core'),
-            'bold' => $OUTPUT->pix_url('e/bold', 'core'),
-            'charmap' => $OUTPUT->pix_url('e/special_character', 'core'),
-            'clear' => $OUTPUT->pix_url('e/clear_formatting', 'core'),
-            'emoticon' => $OUTPUT->pix_url('e/emoticons', 'core'),
-            'equation' => $OUTPUT->pix_url('e/math', 'core'),
-            'fontcolor' => $OUTPUT->pix_url('e/text_color', 'core'),
-            'html' => $OUTPUT->pix_url('e/source_code', 'core'),
-            'image' => $OUTPUT->pix_url('e/insert_edit_image', 'core'),
-            'indent' => array(
-                $OUTPUT->pix_url('e/increase_indent', 'core'),
-                $OUTPUT->pix_url('e/decrease_indent', 'core'),
-            ),
-            'italic' => $OUTPUT->pix_url('e/italic', 'core'),
-            'link' => $OUTPUT->pix_url('e/insert_edit_link', 'core'),
-            'managefiles' => $OUTPUT->pix_url('e/manage_files', 'core'),
-            'media' => $OUTPUT->pix_url('e/insert_edit_video', 'core'),
-            'orderedlist' => $OUTPUT->pix_url('e/numbered_list', 'core'),
-            'rtl' => array($OUTPUT->pix_url('e/left_to_right', 'core'),
-                $OUTPUT->pix_url('e/right_to_left', 'core')),
-            'strike' => $OUTPUT->pix_url('e/strikethrough', 'core'),
-            'subscript' => $OUTPUT->pix_url('e/subscript', 'core'),
-            'superscript' => $OUTPUT->pix_url('e/superscript', 'core'),
-            'table' => $OUTPUT->pix_url('e/table', 'core'),
-            'title' => $OUTPUT->pix_url('e/styleprops', 'core'),
-            'underline' => $OUTPUT->pix_url('e/underline', 'core'),
-            'undo' => array($OUTPUT->pix_url('e/undo', 'core'), $OUTPUT->pix_url('e/redo', 'core')),
-            'unlink' => $OUTPUT->pix_url('e/remove_link', 'core'),
-            'unorderedlist' => $OUTPUT->pix_url('e/bullet_list', 'core')
-        );
-
         // Iterate through subplugins.
         foreach ($subplugins as $name => $dir) {
             $namestr = get_string('pluginname', 'atto_' . $name);
@@ -181,30 +215,14 @@ class atto_subplugins_settings extends admin_setting {
 
             $displayname = $namestr;
 
-            // Check if there is a pix folder in the atto plugin.
+            // Check if there is an icon in the atto plugin pix/ folder.
             if ($PAGE->theme->resolve_image_location('icon', 'atto_' . $name, false)) {
                 $icon = $OUTPUT->pix_icon('icon', '', 'atto_' . $name, array('class' => 'icon pluginicon'));
             } else {
-                // Attempt to find out the icons for core atto plugins.
-                if (array_key_exists($name, $corepluginicons)) {
-                    // It's a core plugin.
-                    $icons = array();
-                    if (!is_array($corepluginicons[$name])) {
-                        $icons[] = $corepluginicons[$name];
-                    } else {
-                        $icons = $corepluginicons[$name];
-                    }
-                    $icon = '';
-                    foreach ($icons as $anicon) {
-                        $icon .= html_writer::empty_tag('img', array('src' => $anicon,
-                            "class" => "pluginicon", "alt" => $displayname));
-                    }
-                } else {
-                    // No icon found.
-                    $icon = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'icon pluginicon noicon'));
-                }
+                // No icon found.
+                $icon = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'icon pluginicon noicon'));
             }
-            $displayname  = $icon . ' ' . $displayname;
+            $displayname = $icon . $displayname;
 
             // Add settings link.
             if (!$version) {
index 61ceb0f..410711d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['errorcannotparseline'] = 'The line \'{$a}\' could not be parsed.';
+$string['errorgroupisusedtwice'] = 'The group \'{$a}\' is defined twice, group names must be unique.';
+$string['errornopluginsorgroupsfound'] = 'No plugins or groups found, please add some groups and plugins.';
+$string['errorpluginnotfound'] = 'The plugin \'{$a}\' cannot be used, it does not appear to be installed.';
+$string['errorpluginisusedtwice'] = 'The plugin \'{$a}\' is used twice, plugins can only be defined once.';
 $string['pluginname'] = 'Atto HTML editor';
 $string['subplugintype_atto'] = 'Atto plugin';
 $string['subplugintype_atto_plural'] = 'Atto plugins';
 $string['settings'] = 'Atto toolbar settings';
 $string['toolbarconfig'] = 'Toolbar config';
-$string['toolbarconfig_desc'] = 'The list of plugins and the order they are displayed can be configured here. The configuration consists of groups (one per line) followed by the ordered list of plugins for that group. The group is separated from the plugins with an equals sign and the plugins are separated with commas. The group must be one of the following: style, paragraph, links, insert, other.';
+$string['toolbarconfig_desc'] = 'The list of plugins and the order they are displayed can be configured here. The configuration consists of groups (one per line) followed by the ordered list of plugins for that group. The group is separated from the plugins with an equals sign and the plugins are separated with commas. The group names must be unique and should indicate what the buttons have in common. Button and group names should not be repeated and may only contain alphanumeric characters.';
index 72e2dbb..28a90bb 100644 (file)
@@ -26,4 +26,4 @@ $string['pluginname'] = 'Accessibility checker';
 $string['nowarnings'] = 'Congratulations, no accessibility problems found!';
 $string['report'] = 'Accessibility report:';
 $string['imagesmissingalt'] = 'Images require alternative text. To fix this warning, add an alt attribute to your img tags. An empty alt attribute may be used, but only when the image is purely decorative and carries no information.';
-$string['needsmorecontrast'] = 'The colors of the foreground and background text do not have enough contrast. To fix this warning, change either foreground or background color of the text so that it is easier to read.';
+$string['needsmorecontrast'] = 'The colours of the foreground and background text do not have enough contrast. To fix this warning, change either foreground or background colour of the text so that it is easier to read.';
index 382ce62..0c7b763 100644 (file)
@@ -22,4 +22,4 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['pluginname'] = 'Background color';
+$string['pluginname'] = 'Background colour';
index e81a608..61103f0 100644 (file)
@@ -26,5 +26,5 @@ $string['pluginname'] = 'Show/hide advanced buttons';
 $string['showmore'] = 'Show more buttons';
 $string['showfewer'] = 'Show fewer buttons';
 $string['settings'] = 'Collapse toolbar settings';
-$string['showgroups'] = 'Show (n) groups when collapsed.';
+$string['showgroups'] = 'Show first (n) groups when collapsed.';
 $string['showgroups_desc'] = 'When the toolbar is collapsed (it is by default) only this many groups will be displayed at once.';
index b0ec620..88398dd 100644 (file)
@@ -22,8 +22,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['editequation'] = 'Edit equation';
-$string['editequation_desc'] = 'Equations are written in <a target="_blank" href="http://en.wikibooks.org/wiki/TeX" title="Link to wikipedia">TeX.</a>';
+$string['editequation'] = 'Edit equation using <a href="{$a}" target="_blank">TeX</a>';
 $string['librarygroup1'] = 'Operators';
 $string['librarygroup1_desc'] = 'List of tex commands to list on the operators tab.';
 $string['librarygroup2'] = 'Arrows';
index 58ccf54..3e2816e 100644 (file)
@@ -34,7 +34,6 @@ function atto_equation_strings_for_js() {
     $PAGE->requires->strings_for_js(array('saveequation',
                                           'editequation',
                                           'preview',
-                                          'editequation_desc',
                                           'update',
                                           'librarygroup1',
                                           'librarygroup2',
@@ -82,5 +81,6 @@ function atto_equation_params_for_js($elementid, $options, $fpoptions) {
                 'elements' => get_config('atto_equation', 'librarygroup4'),
             ));
 
-    return array('texfilteractive' => $texfilteractive, 'contextid'=>$context->id, 'library'=>$library);
+    return array('texfilteractive' => $texfilteractive, 'contextid' => $context->id, 'library' => $library,
+        'texdocsurl' => get_docs_url('Using_TeX_Notation'));
 }
index 595c4cf..ad450d7 100644 (file)
@@ -10,7 +10,8 @@
 }
 
 .atto_equation_library button {
-    margin: 4px;
+    margin: 0.25%;
+    min-width: 12%;
 }
 
 #page-admin-setting-atto_equation_settings .form-defaultinfo {
index bc98c72..00abb9c 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js differ
index 53a447e..c043250 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js differ
index 540c061..76df89c 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js differ
index 6bbe8fd..a5af93d 100644 (file)
@@ -50,9 +50,8 @@ var COMPONENTNAME = 'atto_equation',
         FORM: '' +
             '<form class="atto_form">' +
                 '{{{library}}}' +
-                '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{get_string "editequation" component}}</label>' +
+                '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' +
                 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
-                '<p>{{{get_string "editequation_desc" component}}}</p>' +
                 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
                 '<div class="fullwidth {{CSS.EQUATION_PREVIEW}}" id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
                 '<div class="mdl-align">' +
@@ -144,7 +143,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
 
         var dialogue = this.getDialogue({
             headerContent: M.util.get_string('pluginname', COMPONENTNAME),
-            focusAfterHide: true
+            focusAfterHide: true,
+            width: 600
         });
 
         var content = this._getDialogueContent();
@@ -315,6 +315,7 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             elementid: this.get('host').get('elementid'),
             component: COMPONENTNAME,
             library: library,
+            texdocsurl: this.get('texdocsurl'),
             CSS: CSS
         }));
 
@@ -450,6 +451,16 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
          */
         library: {
             value: {}
+        },
+
+        /**
+         * The link to the Moodle Docs page about TeX.
+         *
+         * @attribute texdocsurl
+         * @type string
+         */
+        texdocsurl: {
+            value: null
         }
     }
 });
index c55415c..4cc7f20 100644 (file)
@@ -22,4 +22,4 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['pluginname'] = 'Font color';
+$string['pluginname'] = 'Font colour';
index cd8eb4d..424312f 100644 (file)
@@ -34,12 +34,13 @@ $string['alignment_textbottom'] = 'Text bottom';
 $string['alignment_texttop'] = 'Text top';
 $string['alignment_top'] = 'Top';
 $string['browserepositories'] = 'Browse repositories...';
+$string['constrain'] = 'Keep ratio';
 $string['createimage'] = 'Insert image';
-$string['enteralt'] = 'Enter alternative text';
+$string['enteralt'] = 'Describe this image for someone who cannot see it';
 $string['enterurl'] = 'Enter URL';
 $string['height'] = 'Height';
-$string['presentation'] = 'Decorative only';
+$string['presentation'] = 'Description not necessary';
 $string['pluginname'] = 'Image';
-$string['presentationoraltrequired'] = 'Images must be marked as decorative only, or have suitable alternative text.';
+$string['presentationoraltrequired'] = 'Images must have a description, except if the description is marked as not necessary.';
 $string['preview'] = 'Preview';
 $string['width'] = 'Width';
index 1a650fa..a3279e7 100644 (file)
@@ -43,12 +43,12 @@ function atto_image_strings_for_js() {
         'alignment_texttop',
         'alignment_top',
         'browserepositories',
+        'constrain',
         'createimage',
         'enterurl',
         'enteralt',
         'height',
         'presentation',
-        'preview',
         'presentationoraltrequired',
         'width',
     );
index 9531011..61d4776 100644 (file)
@@ -1,4 +1,23 @@
 .atto_image_preview {
     max-width: 150px;
     max-height: 150px;
+    margin-bottom: 1em;
+}
+
+.editor_atto_content img {
+    cursor: pointer;
+}
+
+.atto_image_size {
+    display: inline-block;
+}
+.atto_image_size input[type=checkbox] {
+    margin-left: 1em;
+    margin-right: 1em;
+}
+.atto_image_size input[type=text] {
+    width: 3em;
+}
+.atto_image_size label {
+    display: inline-block;
 }
index 011f06a..3fb7392 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js differ
index 3aadb25..2668335 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js differ
index 011f06a..3fb7392 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js differ
index 131ffc5..77c7991 100644 (file)
@@ -37,10 +37,12 @@ var CSS = {
         INPUTHEIGHT: 'atto_image_heightentry',
         INPUTSUBMIT: 'atto_image_urlentrysubmit',
         INPUTURL: 'atto_image_urlentry',
+        INPUTSIZE: 'atto_image_size',
         INPUTWIDTH: 'atto_image_widthentry',
         IMAGEALTWARNING: 'atto_image_altwarning',
         IMAGEBROWSER: 'openimagebrowser',
         IMAGEPRESENTATION: 'atto_image_presentation',
+        INPUTCONSTRAIN: 'atto_image_constrain',
         IMAGEPREVIEW: 'atto_image_preview'
     },
     ALIGNMENTS = [
@@ -117,15 +119,20 @@ var COMPONENTNAME = 'atto_image',
                 '<label class="sameline" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">{{get_string "presentation" component}}</label>' +
                 '<br/>' +
 
-                // Add the width entry box.
-                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
-                '<input type="text" class="{{CSS.INPUTWIDTH}} id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="10"/>' +
-                '<br/>' +
+                // Add the size entry boxes.
+                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTSIZE}}">Size</label>' +
+                '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="{{CSS.INPUTSIZE}}">' +
+                '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
+                '<input type="text" class="{{CSS.INPUTWIDTH}} input-mini" id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x ' +
 
                 // Add the height entry box.
-                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
-                '<input type="text" class="{{CSS.INPUTHEIGHT}}" id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="10"/>' +
-                '<br/>' +
+                '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
+                '<input type="text" class="{{CSS.INPUTHEIGHT}} input-mini" id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
+
+                // Add the constrain checkbox.
+                '<input type="checkbox" class="{{CSS.INPUTCONSTRAIN}} sameline" id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
+                '<label for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">{{get_string "constrain" component}}</label>' +
+                '</div>' +
 
                 // Add the alignment selector.
                 '<label class="sameline" for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
@@ -137,7 +144,6 @@ var COMPONENTNAME = 'atto_image',
                 '<br/>' +
 
                 // Add the image preview.
-                '<label for="{{elementid}}_{{CSS.IMAGEPREVIEW}}">{{get_string "preview" component}}</label>' +
                 '<div class="mdl-align">' +
                 '<img src="#" class="{{CSS.IMAGEPREVIEW}}" id="{{elementid}}_{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
                 '<br/>' +
@@ -184,6 +190,24 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _form: null,
 
+    /**
+     * Remember the image true dimensions so we can constrain resizing.
+     *
+     * @param _imageRawWidth
+     * @type Integer
+     * @private
+     */
+    _imageRawWidth: 0,
+
+    /**
+     * Remember the image true dimensions so we can constrain resizing.
+     *
+     * @param _imageRawHeight
+     * @type Integer
+     * @private
+     */
+    _imageRawHeight: 0,
+
     initializer: function() {
         this.addButton({
             icon: 'e/insert_edit_image',
@@ -191,6 +215,22 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             tags: 'img',
             tagMatchRequiresAll: false
         });
+        this.editor.delegate('dblclick', this._handleDoubleClick, 'img', this);
+    },
+
+    /**
+     * Handle a double click on an image.
+     *
+     * @method _handleDoubleClick
+     * @param {EventFacade} e
+     * @private
+     */
+    _handleDoubleClick: function(e) {
+        var image = e.target;
+
+        var selection = this.get('host').getSelectionFromNode(image);
+        this.get('host').setSelection(selection);
+        this._displayDialogue();
     },
 
     /**
@@ -217,6 +257,57 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
                 .show();
     },
 
+    /**
+     * Set the inputs for width and height if they are not set, and calculate
+     * if the constrain checkbox should be checked or not.
+     *
+     * @method _loadPreviewImage
+     * @param {String} url
+     * @private
+     */
+    _loadPreviewImage: function(url) {
+        var image = new Image(), self = this;
+
+        image.onload = function() {
+            var input, currentwidth, currentheight, widthRatio, heightRatio;
+
+            self._imageRawWidth = this.width;
+            self._imageRawHeight = this.height;
+
+            input = self._form.one('.' + CSS.INPUTWIDTH);
+            currentwidth = input.get('value');
+            if (currentwidth === '') {
+                input.set('value', this.width);
+                currentwidth = this.width;
+            }
+            input = self._form.one('.' + CSS.INPUTHEIGHT);
+            currentheight = input.get('value');
+            if (currentheight === '') {
+                input.set('value', this.height);
+                currentheight = this.height;
+            }
+            input = self._form.one('.' + CSS.IMAGEPREVIEW);
+            input.set('src', this.src);
+            input.setStyle('display', 'inline');
+
+            if (this.width === 0) {
+                this.width = 1;
+            }
+            if (this.height === 0) {
+                this.height = 1;
+            }
+            input = self._form.one('.' + CSS.INPUTCONSTRAIN);
+            widthRatio = Math.round(parseInt(currentwidth, 10) / this.width);
+            heightRatio = Math.round(parseInt(currentheight, 10) / this.height);
+            input.set('checked', widthRatio === heightRatio);
+
+            // Centre the dialogue once the preview image has loaded.
+            self.getDialogue().centerDialogue();
+        };
+
+        image.src = url;
+    },
+
     /**
      * Return the dialogue content for the tool, attaching any required
      * events.
@@ -227,11 +318,12 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _getDialogueContent: function() {
         var template = Y.Handlebars.compile(TEMPLATE),
+            canShowFilepicker = this.get('host').canShowFilepicker('image'),
             content = Y.Node.create(template({
                 elementid: this.get('host').get('elementid'),
                 CSS: CSS,
                 component: COMPONENTNAME,
-                showFilepicker: this.get('host').canShowFilepicker('image'),
+                showFilepicker: canShowFilepicker,
                 alignments: ALIGNMENTS
             }));
 
@@ -240,15 +332,69 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         // Configure the view of the current image.
         this._applyImageProperties(this._form);
 
+        this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
+        this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustHeight, this);
+        this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustWidth, this);
         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
-        this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
-                this.get('host').showFilepicker('image', this._filepickerCallback, this);
-        }, this);
+
+        if (canShowFilepicker) {
+            this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
+                    this.get('host').showFilepicker('image', this._filepickerCallback, this);
+            }, this);
+        }
 
         return content;
     },
 
+    /**
+     * Adjust the height to keep the aspect ratio.
+     *
+     * @method _autoAdjustHeight
+     * @private
+     */
+    _autoAdjustHeight: function() {
+        var currentWidth, newHeight, currentHeight;
+
+        if (!this._form.one('.' + CSS.INPUTCONSTRAIN).get('checked')) {
+            currentWidth = parseInt(this._form.one('.' + CSS.INPUTWIDTH).get('value'), 10);
+            currentHeight = parseInt(this._form.one('.' + CSS.INPUTHEIGHT).get('value'), 10);
+            this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('height', currentHeight);
+            this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('width', currentWidth);
+            return;
+        }
+        currentWidth = parseInt(this._form.one('.' + CSS.INPUTWIDTH).get('value'), 10);
+        newHeight = Math.round((currentWidth / this._imageRawWidth) * this._imageRawHeight);
+
+        this._form.one('.' + CSS.INPUTHEIGHT).set('value', newHeight);
+        this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('height', newHeight);
+        this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('width', currentWidth);
+    },
+
+    /**
+     * Adjust the width to keep the aspect ratio.
+     *
+     * @method _autoAdjustWidth
+     * @private
+     */
+    _autoAdjustWidth: function() {
+        var currentHeight, newWidth;
+
+        if (!this._form.one('.' + CSS.INPUTCONSTRAIN).get('checked')) {
+            currentWidth = parseInt(this._form.one('.' + CSS.INPUTWIDTH).get('value'), 10);
+            currentHeight = parseInt(this._form.one('.' + CSS.INPUTHEIGHT).get('value'), 10);
+            this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('height', currentHeight);
+            this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('width', currentWidth);
+            return;
+        }
+        currentHeight = parseInt(this._form.one('.' + CSS.INPUTHEIGHT).get('value'), 10);
+        newWidth = Math.round((currentHeight / this._imageRawHeight) * this._imageRawWidth);
+
+        this._form.one('.' + CSS.INPUTWIDTH).set('value', newWidth);
+        this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('width', newWidth);
+        this._form.one('.' + CSS.IMAGEPREVIEW).setAttribute('height', currentHeight);
+    },
+
     /**
      * Update the dialogue after an image was selected in the File Picker.
      *
@@ -259,22 +405,15 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _filepickerCallback: function(params) {
         if (params.url !== '') {
-            var input = this._form.one('.' + CSS.INPUTURL),
-                self = this;
+            var input = this._form.one('.' + CSS.INPUTURL);
             input.set('value', params.url);
 
             // Auto set the width and height.
-            var image = new Image();
-            image.onload = function() {
-                self._form.one('.' + CSS.INPUTWIDTH).set('value', this.width);
-                self._form.one('.' + CSS.INPUTHEIGHT).set('value', this.height);
-                self._form.one('.' + CSS.IMAGEPREVIEW).set('src', this.src);
-                self._form.one('.' + CSS.IMAGEPREVIEW).setStyle('display', 'inline');
-
-                // Centre the dialogue once the preview image has loaded.
-                self.getDialogue().centerDialogue();
-            };
-            image.src = params.url;
+            self._form.one('.' + CSS.INPUTWIDTH).set('value', '');
+            self._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
+
+            // Load the preview image.
+            this._loadPreviewImage(params.url);
         }
     },
 
@@ -311,7 +450,7 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         }
         if (properties.src) {
             form.one('.' + CSS.INPUTURL).set('value', properties.src);
-            img.setAttribute('src', properties.src);
+            this._loadPreviewImage(properties.src);
         }
         if (properties.presentation) {
             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
@@ -386,31 +525,11 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _urlChanged: function() {
-        var input = this._form.one('.' + CSS.INPUTURL),
-            self = this;
+        var input = this._form.one('.' + CSS.INPUTURL);
 
         if (input.get('value') !== '') {
-            // Auto set the width and height.
-            var image = new Image();
-            image.onload = function() {
-                var input;
-
-                input = self._form.one('.' + CSS.INPUTWIDTH);
-                if (input.get('value') === '') {
-                    input.set('value', this.width);
-                }
-                input = self._form.one('.' + CSS.INPUTHEIGHT);
-                if (input.get('value') === '') {
-                    input.set('value', this.height);
-                }
-                input = self._form.one('.' + CSS.IMAGEPREVIEW);
-                input.set('src', this.src);
-                input.setStyle('display', 'inline');
-
-                // Centre the dialogue once the preview image has loaded.
-                self.getDialogue().centerDialogue();
-            };
-            image.src = input.get('value');
+            // Load the preview image.
+            this._loadPreviewImage(input.get('value'));
         }
     },
 
index dba9b7c..d1a2d28 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js differ
index 55b6e5e..48f7ef8 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js differ
index dba9b7c..d1a2d28 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js differ
index 0738fb2..44a2a8f 100644 (file)
@@ -67,10 +67,12 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
     _content: null,
 
     initializer: function() {
-        this.addButton({
-            icon: 'e/insert_edit_video',
-            callback: this._displayDialogue
-        });
+        if (this.get('host').canShowFilepicker('media')) {
+            this.addButton({
+                icon: 'e/insert_edit_video',
+                callback: this._displayDialogue
+            });
+        }
     },
 
     /**
index 388e14e..64190e5 100644 (file)
@@ -22,7 +22,6 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['accessibilityhint'] = '<h3>Accessible tables recommendation</h3><p>An accessible HTML table should have :<ul><li>a caption describing the content of the table</li><li>row and/or column headers</li><li>no empty table headers</li><li>no merged table cells</li></ul>.</p>';
 $string['createtable'] = 'Create table';
 $string['pluginname'] = 'Table';
 $string['numberofcolumns'] = 'Number of columns';
@@ -33,8 +32,8 @@ $string['columns'] = 'Columns';
 $string['rows'] = 'Rows';
 $string['both'] = 'Both';
 $string['edittable'] = 'Edit table';
-$string['addrowafter'] = 'Add row after';
-$string['addcolumnafter'] = 'Add column after';
+$string['addrowafter'] = 'Insert row after current cell';
+$string['addcolumnafter'] = 'Insert column after current cell';
 $string['moverowup'] = 'Move row up';
 $string['moverowdown'] = 'Move row down';
 $string['movecolumnleft'] = 'Move column left';
index cca2098..500421a 100644 (file)
@@ -31,7 +31,6 @@ function atto_table_strings_for_js() {
     global $PAGE;
 
     $PAGE->requires->strings_for_js(array('createtable',
-                                          'accessibilityhint',
                                           'headers',
                                           'caption',
                                           'columns',
index 9cf781f..8e9c9a5 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js differ
index da3625d..5fd06f0 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js differ
index 9cf781f..8e9c9a5 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js differ
index f0e52b7..5cc18aa 100644 (file)
@@ -35,7 +35,7 @@ var COMPONENT = 'atto_table',
     TEMPLATE = '' +
         '<form class="atto_form">' +
             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
-            '<textarea class="caption" id="{{elementid}}_atto_table_caption" rows="4" class="fullwidth" required></textarea>' +
+            '<input class="caption fullwidth" id="{{elementid}}_atto_table_caption" required />' +
             '<br/>' +
             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
             '<select class="headers" id="{{elementid}}_atto_table_headers">' +
@@ -54,8 +54,7 @@ var COMPONENT = 'atto_table',
                 '<br/>' +
                 '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
             '</div>' +
-        '</form>' +
-        '<hr/>{{{get_string "accessibilityhint" component}}}',
+        '</form>',
     CONTEXTMENUTEMPLATE = '' +
         '<ul>' +
             '<li><a href="#" data-change="addcolumnafter">{{get_string "addcolumnafter" component}}</a></li>' +
@@ -295,7 +294,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
 
         // Show the context menu, and align to the current position.
         this._contextMenu.show();
-        this._contextMenu.align(e.tableCell, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
+        this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
 
         // If there are any anchors in the bounding box, focus on the first.
         if (boundingBox.one('a')) {
index 50a76a4..81ceac4 100644 (file)
Binary files a/lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-debug.js and b/lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-debug.js differ
index 0c1a4ef..c93d216 100644 (file)
Binary files a/lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-min.js and b/lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button-min.js differ
index 50a76a4..f13bced 100644 (file)
Binary files a/lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button.js and b/lib/editor/atto/plugins/undo/yui/build/moodle-atto_undo-button/moodle-atto_undo-button.js differ
index d95b725..026e4a2 100644 (file)
@@ -23,6 +23,8 @@
  * @module moodle-atto_undo-button
  */
 
+var LOGNAME = 'moodle-atto_undo-button';
+
 /**
  * Atto text editor undo plugin.
  *
@@ -84,36 +86,170 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
             keys: 89
         });
 
-        this.get('host').on('atto:selectionchanged', this._changeListener, this);
+        // Enable the undo once everything has loaded.
+        this.get('host').on('pluginsloaded', function() {
+            // Adds the current value to the stack.
+            this._addToUndo(this._getHTML());
+            this.get('host').on('atto:selectionchanged', this._changeListener, this);
+        }, this);
+
+        this._updateButtonsStates();
     },
 
     /**
-     * Handle a click on undo
+     * Adds an element to the redo stack.
      *
-     * @method _undoHandler
+     * @method _addToRedo
      * @private
+     * @param {String} html The HTML content to save.
      */
-    _undoHandler: function() {
-        var html = this.editor.getHTML();
-
+    _addToRedo: function(html) {
         this._redoStack.push(html);
-        var last = this._undoStack.pop();
-        if (last === html) {
-            last = this._undoStack.pop();
+    },
+
+    /**
+     * Adds an element to the undo stack.
+     *
+     * @method _addToUndo
+     * @private
+     * @param {String} html The HTML content to save.
+     * @param {Boolean} [clearRedo=false] Whether or not we should clear the redo stack.
+     */
+    _addToUndo: function(html, clearRedo) {
+        var last = this._undoStack[this._undoStack.length - 1];
+
+        if (typeof clearRedo === 'undefined') {
+            clearRedo = false;
+        }
+
+        if (typeof last === 'undefined') {
+            Y.log('Oops, nothing was in the undo stack! There should always be something in there.', 'warn', LOGNAME);
         }
-        if (last) {
-            this.editor.setHTML(last);
-            // Put it back in the undo stack so a new event wont clear the redo stack.
-            this._undoStack.push(last);
-            this.highlightButtons('redo');
+
+        if (last !== html) {
+            this._undoStack.push(html);
+            if (clearRedo) {
+                this._redoStack = [];
+            }
         }
 
+        while (this._undoStack.length > this._maxUndos) {
+            this._undoStack.shift();
+        }
+    },
+
+    /**
+     * Get the editor HTML.
+     *
+     * @method _getHTML
+     * @private
+     * @return {String} The HTML.
+     */
+    _getHTML: function() {
+        return this.get('host').getCleanHTML();
+    },
+
+    /**
+     * Get an element on the redo stack.
+     *
+     * @method _getRedo
+     * @private
+     * @return {String} The HTML to restore, or undefined.
+     */
+    _getRedo: function() {
+        return this._redoStack.pop();
+    },
+
+    /**
+     * Get an element on the undo stack.
+     *
+     * @method _getUndo
+     * @private
+     * @param {String} current The current HTML.
+     * @return {String} The HTML to restore.
+     */
+    _getUndo: function(current) {
+        if (this._undoStack.length === 1) {
+            return this._undoStack[0];
+        }
+
+        last = this._undoStack.pop();
+        if (last === current) {
+            // Oops, the latest undo step is the current content, we should unstack once more.
+            // There is no need to do that in a loop as the same stack should never contain duplicates.
+            last = this._undoStack.pop();
+        }
+
+        // We always need to keep the first element of the stack.
         if (this._undoStack.length === 0) {
-            // If there are no undos left, unhighlight the undo button.
-            this.unHighlightButtons('undo');
+            this._addToUndo(last);
+        }
+
+        return last;
+    },
+
+    /**
+     * Restore a value from a stack.
+     *
+     * @method _restoreValue
+     * @private
+     * @param {String} html The HTML to restore in the editor.
+     */
+    _restoreValue: function(html) {
+        this.editor.setHTML(html);
+        // We always add the restored value to the stack, otherwise an event could think that
+        // the content has changed and clear the redo stack.
+        this._addToUndo(html);
+    },
+
+    /**
+     * Update the states of the buttons.
+     *
+     * @method _updateButtonsStates
+     * @private
+     */
+    _updateButtonsStates: function() {
+        if (this._undoStack.length > 1) {
+            this.enableButtons('undo');
+        } else {
+            this.disableButtons('undo');
+        }
+
+        if (this._redoStack.length > 0) {
+            this.enableButtons('redo');
+        } else {
+            this.disableButtons('redo');
         }
     },
 
+    /**
+     * Handle a click on undo
+     *
+     * @method _undoHandler
+     * @param {Event} The click event
+     * @private
+     */
+    _undoHandler: function(e) {
+        e.preventDefault();
+        var html = this._getHTML(),
+            undo = this._getUndo(html);
+
+        // Edge case, but that could happen. We do nothing when the content equals the undo step.
+        if (html === undo) {
+            this._updateButtonsStates();
+            return;
+        }
+
+        // Restore the value.
+        this._restoreValue(undo);
+
+        // Add to the redo stack.
+        this._addToRedo(html);
+
+        // Update the button states.
+        this._updateButtonsStates();
+    },
+
     /**
      * Handle a click on redo
      *
@@ -123,12 +259,19 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
      */
     _redoHandler: function(e) {
         e.preventDefault();
-        var html = this.editor.getHTML();
+        var html = this._getHTML(),
+            redo = this._getRedo();
+
+        // Edge case, but that could happen. We do nothing when the content equals the redo step.
+        if (html === redo) {
+            this._updateButtonsStates();
+            return;
+        }
+        // Restore the value.
+        this._restoreValue(redo);
 
-        this._undoStack.push(html);
-        var last = this._redoStack.pop();
-        this.editor.setHTML(last);
-        this._undoStack.push(last);
+        // Update the button states.
+        this._updateButtonsStates();
     },
 
     /**
@@ -142,35 +285,15 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
         if (e.event.type.indexOf('key') !== -1) {
             // These are the 4 arrow keys.
             if ((e.event.keyCode !== 39) &&
-                (e.event.keyCode !== 37) &&
-                (e.event.keyCode !== 40) &&
-                (e.event.keyCode !== 38)) {
+                    (e.event.keyCode !== 37) &&
+                    (e.event.keyCode !== 40) &&
+                    (e.event.keyCode !== 38)) {
                 // Skip this event type. We only want focus/mouse/arrow events.
                 return;
             }
         }
 
-        if (typeof this._undoStack === 'undefined') {
-            this._undoStack = [];
-        }
-
-        var last = this._undoStack[this._undoStack.length-1];
-        var html = this.editor.getHTML();
-        if (last !== html) {
-            this._undoStack.push(this.editor.getHTML());
-            this._redoStack = [];
-            this.unHighlightButtons('redo');
-        }
-
-        while (this._undoStack.length > this._maxUndos) {
-            this._undoStack.shift();
-        }
-
-        // Show in the buttons if undo/redo is possible.
-        if (this._undoStack.length) {
-           this.highlightButtons('undo');
-        } else {
-           this.unHighlightButtons('undo');
-        }
+        this._addToUndo(this._getHTML(), true);
+        this._updateButtonsStates();
     }
 });
index 34c0613..3ab4753 100644 (file)
@@ -29,7 +29,7 @@ $ADMIN->add('editorsettings', new admin_category('editoratto', $editor->displayn
 $settings = new admin_settingpage('editorsettingsatto', new lang_string('settings', 'editor_atto'));
 if ($ADMIN->fulltree) {
     require_once(__DIR__ . '/adminlib.php');
-    $settings->add(new atto_subplugins_settings());
+    $settings->add(new editor_atto_subplugins_setting());
     $name = new lang_string('toolbarconfig', 'editor_atto');
     $desc = new lang_string('toolbarconfig_desc', 'editor_atto');
     $default = 'collapse = collapse
@@ -44,10 +44,7 @@ insert = equation, charmap, table, clear
 undo = undo
 accessibility = accessibilitychecker, accessibilityhelper
 other = html';
-    $setting = new admin_setting_configtextarea('editor_atto/toolbar',
-                                                    $name,
-                                                    $desc,
-                                                    $default);
+    $setting = new editor_atto_toolbar_setting('editor_atto/toolbar', $name, $desc, $default);
 
     $settings->add($setting);
 }
index a754ce5..008dd7c 100644 (file)
@@ -6,11 +6,6 @@
     padding: 4px;
     resize: vertical;
     overflow: auto;
-    height: 200px;
-}
-/* Resize doesn't work in IE so revert to having the editor grow with the content */
-body.ie .editor_atto_content {
-    height: auto;
 }
 
 .editor_atto_content_wrap,
@@ -137,7 +132,7 @@ div.editor_atto_toolbar div.atto_group {
 }
 .atto_form label {
     display: block;
-    margin: 5px;
+    margin: 0 0 5px 0;
 }
 
 /* RTL Rules */
index be16ef5..5c7a78c 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 6b57aa8..8e72ac4 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 772e7e2..0c14f5f 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 4e5a062..28b4239 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js differ
index 30d52c5..f8d557e 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js differ
index 911a37a..4a51f51 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js differ
index 0763f80..923a8ad 100644 (file)
@@ -474,6 +474,9 @@ EditorPluginButtons.prototype = {
      * @private
      */
     _hideMenu: function(e, menuDialogue) {
+        if (menuDialogue.get('preventHideMenu') === true) {
+            return;
+        }
         menuDialogue.hide();
         new Y.EventHandle(this._menuHideHandlers).detach();
     },
@@ -494,11 +497,18 @@ EditorPluginButtons.prototype = {
             // And the normalized callback configuration.
             buttonConfig = this._normalizeCallback(config.items[index], config.globalItemConfig);
 
-        // Clear the focus after hide so that focus is returned to the editor and changes are made correctly.
-        menuDialogue.set('focusAfterHide', null);
+        // Prevent the dialogue to be closed because of some browser weirdness.
+        menuDialogue.set('preventHideMenu', true);
 
         // Call the callback for this button.
         buttonConfig.callback(e, buttonConfig._callback, buttonConfig.callbackArgs);
+
+        // Cancel the hide menu prevention.
+        menuDialogue.set('preventHideMenu', false);
+
+        // Set the focus after hide so that focus is returned to the editor and changes are made correctly.
+        menuDialogue.set('focusAfterHide', this.get('host').editor);
+        this._hideMenu(e, menuDialogue);
     },
 
     /**
@@ -578,6 +588,7 @@ EditorPluginButtons.prototype = {
      * @param {EventFacade} e
      * @param {Function} callback The function to call which makes the relevant changes.
      * @param {Array} [callbackArgs] The arguments passed to this callback.
+     * @return {Mixed} The value returned by the callback.
      * @private
      */
     _callbackWrapper: function(e, callback, callbackArgs) {
@@ -608,7 +619,7 @@ EditorPluginButtons.prototype = {
         var args = [e, callbackArgs];
 
         // Actually call the callback now.
-        callback.apply(this, args);
+        return callback.apply(this, args);
     },
 
     /**
index d8116b3..09cc4e6 100644 (file)
@@ -92,7 +92,7 @@ EditorPluginDialogue.prototype = {
             if (focusAfterHide === true) {
                 this._dialogue.set('focusAfterHide', this.buttons[this.buttonNames[0]]);
 
-            } else if (typeof setFocusAfter === 'string') {
+            } else if (typeof focusAfterHide === 'string') {
                 this._dialogue.set('focusAfterHide', this.buttons[focusAfterHide]);
 
             } else {
index ce5a2ef..039a4b8 100644 (file)
@@ -188,8 +188,14 @@ Y.extend(Editor, Y.Base, {
         content.appendChild(this.editor);
         this._wrapper.appendChild(content);
 
-        // Style the editor.
-        this.editor.setStyle('minHeight', (1.2 * (this.textarea.getAttribute('rows'))) + 'em');
+        // Style the editor. According to the styles.css: 20 is the line-height, 8 is padding-top + padding-bottom.
+        this.editor.setStyle('minHeight', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
+
+        if (Y.UA.ie === 0) {
+            // We set a height here to force the overflow because decent browsers allow the CSS property resize.
+            this.editor.setStyle('height', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
+        }
+
         // Disable odd inline CSS styles.
         this.disableCssStyling();
 
@@ -255,7 +261,7 @@ Y.extend(Editor, Y.Base, {
             prefix: 'atto'
         });
 
-        Y.delegate(['mouseup', 'keyup', 'focus'], this._hasSelectionChanged, document.body, '.' + CSS.CONTENT, this);
+        Y.delegate(['mouseup', 'keyup', 'focus'], this._hasSelectionChanged, document.body, '#' + this.editor.get('id'), this);
 
         return this;
     },
index 1f918a6..383f825 100644 (file)
@@ -204,7 +204,7 @@ EditorSelection.prototype = {
 
         var editor = this.editor,
             stopFn = function(node) {
-                editor.contains(node);
+                return editor.contains(node);
             };
 
         selectednodes.each(function(node){
index 87bc00a..602be9c 100644 (file)
@@ -34,6 +34,7 @@ $string['customtoolbar_desc'] = 'Each line contains a list of comma separated bu
 $string['fontselectlist'] = 'Available fonts list';
 $string['pluginname'] = 'TinyMCE HTML editor';
 $string['settings'] = 'General settings';
+$string['subplugintype_tinymce'] = 'Plugin';
 $string['subplugintype_tinymce_plural'] = 'Plugins';
 
 
index fbaf876..968934a 100644 (file)
@@ -7665,7 +7665,18 @@ function moodle_setlocale($locale='') {
         $messages= setlocale (LC_MESSAGES, 0);
     }
     // Set locale to all.
-    setlocale (LC_ALL, $currentlocale);
+    $result = setlocale (LC_ALL, $currentlocale);
+    // If setting of locale fails try the other utf8 or utf-8 variant,
+    // some operating systems support both (Debian), others just one (OSX).
+    if ($result === false) {
+        if (stripos($currentlocale, '.UTF-8') !== false) {
+            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
+            setlocale (LC_ALL, $newlocale);
+        } else if (stripos($currentlocale, '.UTF8') !== false) {
+            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
+            setlocale (LC_ALL, $newlocale);
+        }
+    }
     // Set old values.
     setlocale (LC_MONETARY, $monetary);
     setlocale (LC_NUMERIC, $numeric);
index 9f4d8e4..cee68f5 100644 (file)
@@ -666,16 +666,16 @@ class core_renderer extends renderer_base {
             unset($SESSION->justloggedin);
             if (!empty($CFG->displayloginfailures)) {
                 if (!isguestuser()) {
-                    if ($count = count_login_failures($CFG->displayloginfailures, $USER->username, $USER->lastlogin)) {
+                    // Include this file only when required.
+                    require_once($CFG->dirroot . '/user/lib.php');
+                    if ($count = user_count_login_failures($USER)) {
                         $loggedinas .= '&nbsp;<div class="loginfailures">';
-                        if (empty($count->accounts)) {
-                            $loggedinas .= get_string('failedloginattempts', '', $count);
-                        } else {
-                            $loggedinas .= get_string('failedloginattemptsall', '', $count);
-                        }
+                        $a = new stdClass();
+                        $a->attempts = $count;
+                        $loggedinas .= get_string('failedloginattempts', '', $a);
                         if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
-                            $loggedinas .= ' (<a href="'.$CFG->wwwroot.'/report/log/index.php'.
-                                                 '?chooselog=1&amp;id=1&amp;modid=site_errors">'.get_string('logs').'</a>)';
+                            $loggedinas .= html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
+                                    'id' => 0 , 'modid' => 'site_errors')), '(' . get_string('logs') . ')');
                         }
                         $loggedinas .= '</div>';
                     }
index b0efa09..ea316a7 100644 (file)
@@ -226,7 +226,10 @@ EOD;
             $record['deleted'] = 0;
         }
 
-        $record['timecreated'] = time();
+        if (!isset($record['timecreated'])) {
+            $record['timecreated'] = time();
+        }
+
         $record['timemodified'] = $record['timecreated'];
         $record['lastip'] = '0.0.0.0';
 
index ac7cc06..a8856e7 100644 (file)
@@ -416,9 +416,10 @@ class core_accesslib_testcase extends advanced_testcase {
         $event = array_pop($events);
 
         $this->assertInstanceOf('\core\event\role_capabilities_updated', $event);
-        $expectedurl = new moodle_url('admin/roles/define.php', array('action' => 'view', 'roleid' => $student->id));
+        $expectedurl = new moodle_url('/admin/roles/define.php', array('action' => 'view', 'roleid' => $student->id));
         $this->assertEquals($expectedurl, $event->get_url());
         $this->assertEventLegacyLogData($expectedlegacylog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
index ebe592f..174e54c 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * This file contains the unittests for the css optimiser in csslib.php
+ * This file contains the unittests for adhock tasks.
  *
  * @package   core
  * @category  phpunit
@@ -24,6 +24,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
+require_once(__DIR__ . '/fixtures/task_fixtures.php');
 
 
 /**
@@ -34,14 +35,12 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2013 Damyon Wiese
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class adhoc_task_testcase extends advanced_testcase {
+class core_adhoc_task_testcase extends advanced_testcase {
 
     public function test_get_next_adhoc_task() {
-        global $DB;
-
         $this->resetAfterTest(true);
         // Create an adhoc task.
-        $task = new testable_adhoc_task();
+        $task = new \core\task\adhoc_test_task();
 
         // Queue it.
         $task = \core\task\manager::queue_adhoc_task($task);
@@ -69,8 +68,3 @@ class adhoc_task_testcase extends advanced_testcase {
         $this->assertNull($task);
     }
 }
-
-class testable_adhoc_task extends \core\task\adhoc_task {
-    public function execute() {
-    }
-}
diff --git a/lib/tests/event_user_graded_test.php b/lib/tests/event_user_graded_test.php
new file mode 100644 (file)
index 0000000..293b00e
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for base course module viewed event.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2014 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class core_event_user_graded_testcase
+ *
+ * Tests for event \core\event\user_graded
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2014 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_event_user_graded_testcase extends advanced_testcase {
+    /**
+     * Test the event.
+     */
+    public function test_event() {
+        global $CFG;
+        require_once("$CFG->libdir/gradelib.php");
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $grade_category = grade_category::fetch_course_category($course->id);
+        $grade_category->load_grade_item();
+        $grade_item = $grade_category->grade_item;
+
+        $grade_item->update_final_grade($user->id, 10, 'gradebook');
+
+        $grade_grade = new grade_grade(array('userid' => $user->id, 'itemid' => $grade_item->id), true);
+        $grade_grade->grade_item = $grade_item;
+
+        $event = \core\event\user_graded::create_from_grade($grade_grade);
+
+        $this->assertEventLegacyLogData(
+            array($course->id, 'grade', 'update', '/report/grader/index.php?id=' . $course->id, $grade_item->itemname . ': ' . fullname($user)),
+            $event
+        );
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertSame($event->objecttable, 'grade_grades');
+        $this->assertEquals($event->objectid, $grade_grade->id);
+        $this->assertEquals($event->other['itemid'], $grade_item->id);
+        $this->assertTrue($event->other['overridden']);
+        $this->assertEquals(10, $event->other['finalgrade']);
+
+        // Trigger the events.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $result = $sink->get_events();
+        $sink->close();
+
+        $this->assertCount(1, $result);
+
+        $event = reset($result);
+        $this->assertEventContextNotUsed($event);
+
+        $grade = $event->get_grade();
+        $this->assertInstanceOf('grade_grade', $grade);
+        $this->assertEquals($grade_grade->id, $grade->id);
+    }
+}
index cd54a1c..130404d 100644 (file)
@@ -49,6 +49,8 @@ class core_events_testcase extends advanced_testcase {
         // Check that the event data is valid.
         $this->assertInstanceOf('\core\event\course_category_created', $event);
         $this->assertEquals(context_coursecat::instance($category->id), $event->get_context());
+        $url = new moodle_url('/course/management.php', array('categoryid' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $expected = array(SITEID, 'category', 'add', 'editcategory.php?id=' . $category->id, $category->id);
         $this->assertEventLegacyLogData($expected, $event);
         $this->assertEventContextNotUsed($event);
@@ -74,6 +76,8 @@ class core_events_testcase extends advanced_testcase {
         // Check that the event data is valid.
         $this->assertInstanceOf('\core\event\course_category_updated', $event);
         $this->assertEquals(context_coursecat::instance($category->id), $event->get_context());
+        $url = new moodle_url('/course/editcategory.php', array('id' => $event->objectid));
+        $this->assertEquals($url, $event->get_url());
         $expected = array(SITEID, 'category', 'update', 'editcategory.php?id=' . $category->id, $category->id);
         $this->assertEventLegacyLogData($expected, $event);
 
index 9d8e516..885a3f0 100644 (file)
@@ -205,6 +205,7 @@ class problematic_event3 extends \core\event\base {
     }
 
     protected function validate_data() {
+        parent::validate_data();
         if (empty($this->data['other'])) {
             debugging('other is missing');
         }
diff --git a/lib/tests/fixtures/task_fixtures.php b/lib/tests/fixtures/task_fixtures.php
new file mode 100644 (file)
index 0000000..6ec619f
--- /dev/null
@@ -0,0 +1,59 @@
+<?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/>.
+
+/**
+ * Fixtures for task tests.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2014 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\task;
+defined('MOODLE_INTERNAL') || die();
+
+class adhoc_test_task extends \core\task\adhoc_task {
+    public function execute() {
+    }
+}
+
+class scheduled_test_task extends \core\task\scheduled_task {
+    public function get_name() {
+        return "Test task";
+    }
+
+    public function execute() {
+    }
+}
+
+class scheduled_test2_task extends \core\task\scheduled_task {
+    public function get_name() {
+        return "Test task 2";
+    }
+
+    public function execute() {
+    }
+}
+
+class scheduled_test3_task extends \core\task\scheduled_task {
+    public function get_name() {
+        return "Test task 3";
+    }
+
+    public function execute() {
+    }
+}
index 648fb40..e60b99a 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * This file contains the unittests for the css optimiser in csslib.php
+ * This file contains the unittests for scheduled tasks.
  *
  * @package   core
  * @category  phpunit
@@ -24,7 +24,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-
+require_once(__DIR__ . '/fixtures/task_fixtures.php');
 
 /**
  * Test class for scheduled task.
@@ -34,13 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2013 Damyon Wiese
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class scheduled_task_testcase extends advanced_testcase {
+class core_scheduled_task_testcase extends advanced_testcase {
 
     /**
      * Test the cron scheduling method
      */
     public function test_eval_cron_field() {
-        $testclass = new testable_scheduled_task();
+        $testclass = new \core\task\scheduled_test_task();
 
         $this->assertEquals(20, count($testclass->eval_cron_field('*/3', 0, 59)));
         $this->assertEquals(31, count($testclass->eval_cron_field('1,*/2', 0, 59)));
@@ -52,7 +52,7 @@ class scheduled_task_testcase extends advanced_testcase {
 
     public function test_get_next_scheduled_time() {
         // Test job run at 1 am.
-        $testclass = new testable_scheduled_task();
+        $testclass = new \core\task\scheduled_test_task();
 
         // All fields default to '*'.
         $testclass->set_hour('1');
@@ -68,8 +68,13 @@ class scheduled_task_testcase extends advanced_testcase {
 
         $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
 
+        // Disabled flag does not affect next time.
+        $testclass->set_disabled(true);
+        $nexttime = $testclass->get_next_scheduled_time();
+        $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
+
         // Now test for job run every 10 minutes.
-        $testclass = new testable_scheduled_task();
+        $testclass = new \core\task\scheduled_test_task();
 
         // All fields default to '*'.
         $testclass->set_minute('*/10');
@@ -80,6 +85,11 @@ class scheduled_task_testcase extends advanced_testcase {
         $nexttenminutes = mktime(date('H'), $minutes, 0);
 
         $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
+
+        // Disabled flag does not affect next time.
+        $testclass->set_disabled(true);
+        $nexttime = $testclass->get_next_scheduled_time();
+        $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
     }
 
     public function test_timezones() {
@@ -103,7 +113,7 @@ class scheduled_task_testcase extends advanced_testcase {
         // GMT-04:30.
         $CFG->timezone = 'America/Caracas';
 
-        $testclass = new testable_scheduled_task();
+        $testclass = new \core\task\scheduled_test_task();
 
         // Scheduled tasks should always use servertime - so this is 03:30 GMT.
         $testclass->set_hour('1');
@@ -142,23 +152,28 @@ class scheduled_task_testcase extends advanced_testcase {
         $record->day = '*';
         $record->month = '*';
         $record->component = 'test_scheduled_task';
-        $record->classname = '\\testable_scheduled_task';