Merge branch 'MDL-32870' of git://github.com/jmvedrine/moodle
authorMarina Glancy <marina@moodle.com>
Tue, 7 Oct 2014 09:16:56 +0000 (17:16 +0800)
committerMarina Glancy <marina@moodle.com>
Tue, 7 Oct 2014 09:16:56 +0000 (17:16 +0800)
272 files changed:
admin/environment.xml
admin/roles/classes/potential_assignees_below_course.php
admin/settings/grades.php
admin/tool/behat/tests/behat/get_and_set_fields.feature
admin/tool/langimport/classes/controller.php [new file with mode: 0644]
admin/tool/langimport/classes/event/langpack_imported.php [new file with mode: 0644]
admin/tool/langimport/classes/event/langpack_removed.php [new file with mode: 0644]
admin/tool/langimport/classes/event/langpack_updated.php [new file with mode: 0644]
admin/tool/langimport/classes/task/update_langpacks_task.php [new file with mode: 0644]
admin/tool/langimport/db/tasks.php [new file with mode: 0644]
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/behat/behat_tool_langimport.php [new file with mode: 0644]
admin/tool/langimport/tests/behat/manage_langpacks.feature [new file with mode: 0644]
admin/tool/langimport/tests/events_test.php [new file with mode: 0644]
admin/tool/langimport/version.php
admin/tool/messageinbound/classes/edit_handler_form.php [new file with mode: 0644]
admin/tool/messageinbound/classes/manager.php [new file with mode: 0644]
admin/tool/messageinbound/classes/message/inbound/invalid_recipient_handler.php [new file with mode: 0644]
admin/tool/messageinbound/classes/task/cleanup_task.php [new file with mode: 0644]
admin/tool/messageinbound/classes/task/pickup_task.php [new file with mode: 0644]
admin/tool/messageinbound/db/messageinbound_handlers.php [new file with mode: 0644]
admin/tool/messageinbound/db/messages.php [new file with mode: 0644]
admin/tool/messageinbound/db/tasks.php [new file with mode: 0644]
admin/tool/messageinbound/index.php [new file with mode: 0644]
admin/tool/messageinbound/lang/en/tool_messageinbound.php [new file with mode: 0644]
admin/tool/messageinbound/renderer.php [new file with mode: 0644]
admin/tool/messageinbound/settings.php [new file with mode: 0644]
admin/tool/messageinbound/styles.css [new file with mode: 0644]
admin/tool/messageinbound/version.php [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
cohort/assign.php
cohort/edit.php
cohort/edit_form.php
cohort/externallib.php
cohort/index.php
cohort/lib.php
cohort/tests/behat/access_visible_cohorts.feature [new file with mode: 0644]
cohort/tests/behat/upload_cohorts.feature
cohort/tests/cohortlib_test.php
cohort/tests/externallib_test.php
cohort/tests/fixtures/uploadcohorts1.csv
cohort/upload_form.php
course/classes/management/helper.php
course/classes/management_renderer.php
course/management.php
course/renderer.php
course/reset_form.php
course/tests/behat/course_search.feature [new file with mode: 0644]
enrol/cohort/ajax.php [deleted file]
enrol/cohort/edit_form.php
enrol/cohort/lang/en/enrol_cohort.php
enrol/cohort/lib.php
enrol/cohort/locallib.php
enrol/cohort/yui/quickenrolment/assets/skins/sam/quickenrolment.css [deleted file]
enrol/cohort/yui/quickenrolment/assets/skins/sam/sprite.png [deleted file]
enrol/cohort/yui/quickenrolment/quickenrolment.js [deleted file]
enrol/manual/ajax.php
enrol/manual/lang/en/enrol_manual.php
enrol/manual/lib.php
enrol/manual/locallib.php
enrol/manual/yui/quickenrolment/assets/skins/sam/quickenrolment.css
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/self/edit_form.php
grade/edit/scale/edit.php
grade/edit/settings/index.php
grade/edit/tree/action.php
grade/edit/tree/calculation.php
grade/edit/tree/category.php
grade/edit/tree/category_form.php
grade/edit/tree/functions.js
grade/edit/tree/grade_form.php
grade/edit/tree/index.php
grade/edit/tree/item.php
grade/edit/tree/item_form.php
grade/edit/tree/lib.php
grade/edit/tree/outcomeitem.php
grade/lib.php
grade/report/grader/index.php
grade/report/grader/lib.php
grade/report/lib.php
grade/report/overview/lib.php
grade/report/user/lib.php
grade/report/user/settings.php
grade/report/user/styles.css
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_calculated_weights.feature [new file with mode: 0644]
grade/tests/behat/grade_mingrade.feature [new file with mode: 0644]
grade/tests/behat/grade_natural_normalisation.feature [new file with mode: 0644]
grade/tests/behat/grade_scales.feature [new file with mode: 0644]
grade/tests/behat/grade_view.feature
grade/tests/edittreelib_test.php
grade/tests/report_graderlib_test.php
grade/tests/reportlib_test.php
group/autogroup_form.php
lang/en/cohort.php
lang/en/deprecated.txt
lang/en/grades.php
lib/classes/grades_external.php
lib/classes/message/inbound/handler.php
lib/classes/message/inbound/processing_failed_exception.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/classes/task/scheduled_task.php
lib/db/access.php
lib/db/install.xml [changed mode: 0644->0755]
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/grade/constants.php
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/grade/grade_item.php
lib/gradelib.php
lib/phpunit/classes/util.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_general.php
lib/tests/scheduled_task_test.php
lib/upgrade.txt
message/externallib.php
message/lib.php
message/tests/externallib_test.php
message/tests/fixtures/inbound_fixtures.php
mod/forum/classes/existing_subscriber_selector.php
mod/forum/classes/potential_subscriber_selector.php
mod/forum/classes/subscriptions.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lib.php
mod/forum/tests/behat/forum_subscriptions_availability.feature [new file with mode: 0644]
mod/lesson/db/install.xml
mod/lesson/db/upgrade.php
mod/lesson/lib.php
mod/lesson/mod_form.php
mod/lesson/settings.php
mod/lesson/tests/behat/date_availability.feature
mod/lesson/tests/behat/lesson_navigation.feature
mod/lesson/tests/behat/lesson_review.feature
mod/lesson/tests/behat/password_protection.feature
mod/lesson/tests/behat/time_limit.feature
mod/lesson/version.php
mod/quiz/addrandom.php
mod/quiz/addrandomform.php
mod/quiz/attemptlib.php
mod/quiz/classes/admin_review_setting.php
mod/quiz/classes/output/edit_renderer.php [new file with mode: 0644]
mod/quiz/classes/repaginate.php [new file with mode: 0644]
mod/quiz/classes/structure.php [new file with mode: 0644]
mod/quiz/edit.js [deleted file]
mod/quiz/edit.php
mod/quiz/edit_rest.php [new file with mode: 0644]
mod/quiz/editlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/mod_form.php
mod/quiz/questionbank.ajax.php [new file with mode: 0644]
mod/quiz/renderer.php
mod/quiz/repaginate.php [new file with mode: 0644]
mod/quiz/styles.css
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/editing_add.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_click_move_icon.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_repaginate.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_set_marks.feature [new file with mode: 0644]
mod/quiz/tests/editlib_test.php [deleted file]
mod/quiz/tests/lib_test.php
mod/quiz/tests/locallib_test.php
mod/quiz/tests/repaginate_test.php [new file with mode: 0644]
mod/quiz/tests/structure_test.php [new file with mode: 0644]
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizquestionbank/moodle-mod_quiz-quizquestionbank-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizquestionbank/moodle-mod_quiz-quizquestionbank-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-quizquestionbank/moodle-mod_quiz-quizquestionbank.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-randomquestion/moodle-mod_quiz-randomquestion-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-randomquestion/moodle-mod_quiz-randomquestion-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-randomquestion/moodle-mod_quiz-randomquestion.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/build.json [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/js/dragdrop.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/js/resource.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/js/section.js [new file with mode: 0644]
mod/quiz/yui/src/dragdrop/meta/dragdrop.json [new file with mode: 0644]
mod/quiz/yui/src/modform/build.json [new file with mode: 0644]
mod/quiz/yui/src/modform/js/modform.js [new file with mode: 0644]
mod/quiz/yui/src/modform/meta/modform.json [new file with mode: 0644]
mod/quiz/yui/src/questionchooser/build.json [new file with mode: 0644]
mod/quiz/yui/src/questionchooser/js/questionchooser.js [new file with mode: 0644]
mod/quiz/yui/src/questionchooser/meta/questionchooser.json [new file with mode: 0644]
mod/quiz/yui/src/quizbase/build.json [new file with mode: 0644]
mod/quiz/yui/src/quizbase/js/quizbase.js [new file with mode: 0644]
mod/quiz/yui/src/quizbase/meta/quizbase.json [new file with mode: 0644]
mod/quiz/yui/src/quizquestionbank/build.json [new file with mode: 0644]
mod/quiz/yui/src/quizquestionbank/js/quizquestionbank.js [new file with mode: 0644]
mod/quiz/yui/src/quizquestionbank/meta/quizquestionbank.json [new file with mode: 0644]
mod/quiz/yui/src/randomquestion/build.json [new file with mode: 0644]
mod/quiz/yui/src/randomquestion/js/randomquestion.js [new file with mode: 0644]
mod/quiz/yui/src/randomquestion/meta/randomquestion.json [new file with mode: 0644]
mod/quiz/yui/src/repaginate/build.json [new file with mode: 0644]
mod/quiz/yui/src/repaginate/js/repaginate.js [new file with mode: 0644]
mod/quiz/yui/src/repaginate/meta/repaginate.json [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/build.json [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/js/resource.js [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/js/section.js [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/js/toolbox.js [new file with mode: 0644]
mod/quiz/yui/src/toolboxes/meta/toolboxes.json [new file with mode: 0644]
mod/quiz/yui/src/util/build.json [new file with mode: 0644]
mod/quiz/yui/src/util/js/base.js [new file with mode: 0644]
mod/quiz/yui/src/util/js/page.js [new file with mode: 0644]
mod/quiz/yui/src/util/js/slot.js [new file with mode: 0644]
mod/quiz/yui/src/util/meta/util.json [new file with mode: 0644]
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/tests/generator/lib.php
mod/workshop/tests/generator_test.php
mod/workshop/tests/locallib_test.php
mod/workshop/version.php
phpunit.xml.dist
pix/e/insert_page_break.png [new file with mode: 0644]
pix/e/insert_page_break.svg [new file with mode: 0644]
pix/e/remove_page_break.png [new file with mode: 0644]
pix/e/remove_page_break.svg [new file with mode: 0644]
pix/t/reset.png [new file with mode: 0644]
pix/t/reset.svg [new file with mode: 0644]
question/addquestion.php
question/classes/bank/checkbox_column.php
question/classes/bank/view.php
question/editlib.php
question/tests/behat/delete_questions.feature
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js
question/yui/src/qbankmanager/js/qbankmanager.js
theme/base/style/grade.css
theme/bootstrapbase/less/moodle/buttons.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/expendable.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/style/moodle.css
version.php
webservice/lib.php

index b1cfc26..38ec017 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="2.8" requires="2.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.1" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="5.4.4" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opensslrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 71664a1..ce60366 100644 (file)
@@ -48,11 +48,12 @@ class core_role_potential_assignees_below_course extends core_role_assign_user_s
         $fields      = 'SELECT ' . $this->required_fields_sql('u');
         $countfields = 'SELECT COUNT(u.id)';
 
-        $sql   = " FROM {user} u
-              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.roleid = :roleid AND ra.contextid = :contextid)
-                  WHERE u.id IN ($enrolsql)
-                        $wherecondition
-                        AND ra.id IS NULL";
+        $sql   = " FROM ($enrolsql) enrolled_users_view
+                   JOIN {user} u ON u.id = enrolled_users_view.id
+              LEFT JOIN {role_assignments} ra ON (ra.userid = enrolled_users_view.id AND
+                                            ra.roleid = :roleid AND ra.contextid = :contextid)
+                  WHERE ra.id IS NULL
+                        $wherecondition";
         $params['contextid'] = $this->context->id;
         $params['roleid'] = $this->roleid;
 
index 2cb6122..84a5296 100644 (file)
@@ -96,11 +96,9 @@ if (has_capability('moodle/grade:manage', $systemcontext)
                          GRADE_AGGREGATE_MODE            =>new lang_string('aggregatemode', 'grades'),
                          GRADE_AGGREGATE_SUM             =>new lang_string('aggregatesum', 'grades'));
 
-        $defaultvisible = array(GRADE_AGGREGATE_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2,
-                                GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_MEDIAN, GRADE_AGGREGATE_MIN,
-                                GRADE_AGGREGATE_MAX, GRADE_AGGREGATE_MODE, GRADE_AGGREGATE_SUM);
+        $defaultvisible = array(GRADE_AGGREGATE_SUM);
 
-        $defaults = array('value'=>GRADE_AGGREGATE_WEIGHTED_MEAN2, 'forced'=>false, 'adv'=>false);
+        $defaults = array('value' => GRADE_AGGREGATE_SUM, 'forced' => false, 'adv' => false);
         $temp->add(new admin_setting_gradecat_combo('grade_aggregation', new lang_string('aggregation', 'grades'), new lang_string('aggregation_help', 'grades'), $defaults, $options));
 
         $temp->add(new admin_setting_configmultiselect('grade_aggregations_visible', new lang_string('aggregationsvisible', 'grades'),
index 780a315..5e281be 100644 (file)
@@ -159,6 +159,7 @@ Feature: Verify that all form fields values can be get and set
     And I add a "Lesson" to section "1"
     And I set the following fields to these values:
       | Name | Test lesson |
+      | Description | Test lesson description |
       | available[enabled] | 1 |
     And I set the field "deadline[enabled]" to "1"
     # Checkbox (AJAX) - Checking "the field matches value" before saving.
diff --git a/admin/tool/langimport/classes/controller.php b/admin/tool/langimport/classes/controller.php
new file mode 100644 (file)
index 0000000..06f815e
--- /dev/null
@@ -0,0 +1,237 @@
+<?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/>.
+
+/**
+ * Lang import controller
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir.'/componentlib.class.php');
+
+/**
+ * Lang import controller
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class controller {
+    /** @var array list of informational messages */
+    public $info;
+    /** @var array  list of error messages */
+    public $errors;
+    /** @var \lang_installer */
+    private $installer;
+    /** @var array languages available on the remote server */
+    public $availablelangs;
+
+    /**
+     * Constructor.
+     */
+    public function __construct() {
+        make_temp_directory('');
+        make_upload_directory('lang');
+
+        $this->info = array();
+        $this->errors = array();
+        $this->installer = new \lang_installer();
+
+        $this->availablelangs = $this->installer->get_remote_list_of_languages();
+    }
+
+    /**
+     * Install language packs provided
+     *
+     * @param string|array $langs array of langcodes or individual langcodes
+     * @param bool $updating true if updating the langpacks
+     * @return int false if an error encountered or
+     * @throws \moodle_exception when error is encountered installing langpack
+     */
+    public function install_languagepacks($langs, $updating = false) {
+        global $CFG;
+
+        $this->installer->set_queue($langs);
+        $results = $this->installer->run();
+
+        $updatedpacks = 0;
+
+        foreach ($results as $langcode => $langstatus) {
+            switch ($langstatus) {
+                case \lang_installer::RESULT_DOWNLOADERROR:
+                    $a       = new \stdClass();
+                    $a->url  = $this->installer->lang_pack_url($langcode);
+                    $a->dest = $CFG->dataroot.'/lang';
+                    $this->errors[] = get_string('remotedownloaderror', 'error', $a);
+                    throw new \moodle_exception('remotedownloaderror', 'error', $a);
+                    break;
+                case \lang_installer::RESULT_INSTALLED:
+                    $updatedpacks++;
+                    if ($updating) {
+                        event\langpack_updated::event_with_langcode($langcode)->trigger();
+                        $this->info[] = get_string('langpackupdated', 'tool_langimport', $langcode);
+                    } else {
+                        $this->info[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
+                        event\langpack_imported::event_with_langcode($langcode)->trigger();
+                    }
+                    break;
+                case \lang_installer::RESULT_UPTODATE:
+                    $this->info[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
+                    break;
+            }
+        }
+
+        return $updatedpacks;
+    }
+
+    /**
+     * Uninstall language pack
+     *
+     * @param string $lang language code
+     * @return bool true if language succesfull installed
+     */
+    public function uninstall_language($lang) {
+        global $CFG;
+
+        $dest1 = $CFG->dataroot.'/lang/'.$lang;
+        $dest2 = $CFG->dirroot.'/lang/'.$lang;
+        $rm1 = false;
+        $rm2 = false;
+        if (file_exists($dest1)) {
+            $rm1 = remove_dir($dest1);
+        }
+        if (file_exists($dest2)) {
+            $rm2 = remove_dir($dest2);
+        }
+
+        if ($rm1 or $rm2) {
+            $this->info[] = get_string('langpackremoved', 'tool_langimport', $lang);
+            event\langpack_removed::event_with_langcode($lang)->trigger();
+            return true;
+        } else {    // Nothing deleted, possibly due to permission error.
+            $this->errors[] = 'An error has occurred, language pack is not completely uninstalled, please check file permissions';
+            return false;
+        }
+    }
+
+    /**
+     * Updated all install language packs with the latest found on servre
+     *
+     * @return bool true if languages succesfully updated.
+     */
+    public function update_all_installed_languages() {
+        global $CFG;
+
+        if (!$availablelangs = $this->installer->get_remote_list_of_languages()) {
+            $this->errors[] = get_string('cannotdownloadlanguageupdatelist', 'error');
+            return false;
+        }
+
+        $md5array = array();    // Convert to (string)langcode => (string)md5.
+        foreach ($availablelangs as $alang) {
+            $md5array[$alang[0]] = $alang[1];
+        }
+
+        // Filter out unofficial packs.
+        $currentlangs = array_keys(get_string_manager()->get_list_of_translations(true));
+        $updateablelangs = array();
+        foreach ($currentlangs as $clang) {
+            if (!array_key_exists($clang, $md5array)) {
+                $noticeok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
+                continue;
+            }
+            $dest1 = $CFG->dataroot.'/lang/'.$clang;
+            $dest2 = $CFG->dirroot.'/lang/'.$clang;
+
+            if (file_exists($dest1.'/langconfig.php') || file_exists($dest2.'/langconfig.php')) {
+                $updateablelangs[] = $clang;
+            }
+        }
+
+        // Filter out packs that have the same md5 key.
+        $neededlangs = array();
+        foreach ($updateablelangs as $ulang) {
+            if (!$this->is_installed_lang($ulang, $md5array[$ulang])) {
+                $neededlangs[] = $ulang;
+            }
+        }
+
+        // Clean-up currently installed versions of the packs.
+        foreach ($neededlangs as $packindex => $pack) {
+            if ($pack == 'en') {
+                continue;
+            }
+
+            // Delete old directories.
+            $dest1 = $CFG->dataroot.'/lang/'.$pack;
+            $dest2 = $CFG->dirroot.'/lang/'.$pack;
+            if (file_exists($dest1)) {
+                if (!remove_dir($dest1)) {
+                    $noticeerror[] = 'Could not delete old directory '.$dest1.', update of '.$pack
+                        .' failed, please check permissions.';
+                    unset($neededlangs[$packindex]);
+                    continue;
+                }
+            }
+            if (file_exists($dest2)) {
+                if (!remove_dir($dest2)) {
+                    $noticeerror[] = 'Could not delete old directory '.$dest2.', update of '.$pack
+                        .' failed, please check permissions.';
+                    unset($neededlangs[$packindex]);
+                    continue;
+                }
+            }
+        }
+
+        try {
+            $updated = $this->install_languagepacks($neededlangs, true);
+        } catch (\moodle_exception $e) {
+            return false;
+        }
+
+        if ($updated) {
+            $this->info[] = get_string('langupdatecomplete', 'tool_langimport');
+        } else {
+            $this->info[] = get_string('nolangupdateneeded', 'tool_langimport');
+        }
+
+        return true;
+    }
+
+    /**
+     * checks the md5 of the zip file, grabbed from download.moodle.org,
+     * against the md5 of the local language file from last update
+     * @param string $lang language code
+     * @param string $md5check md5 to check
+     * @return bool true if installed
+     */
+    public function is_installed_lang($lang, $md5check) {
+        global $CFG;
+        $md5file = $CFG->dataroot.'/lang/'.$lang.'/'.$lang.'.md5';
+        if (file_exists($md5file)) {
+            return (file_get_contents($md5file) == $md5check);
+        }
+        return false;
+    }
+}
+
+
diff --git a/admin/tool/langimport/classes/event/langpack_imported.php b/admin/tool/langimport/classes/event/langpack_imported.php
new file mode 100644 (file)
index 0000000..c8bd149
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * The langimport langpack imported event.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack imported event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string langcode: the langpage pack code.
+ * }
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_imported extends \core\event\base {
+    /**
+     * Create instance of event.
+     *
+     * @param string $langcode
+     * @return langpack_updated
+     */
+    public static function event_with_langcode($langcode) {
+        $data = array(
+            'context' => \context_system::instance(),
+            'other' => array(
+                'langcode' => $langcode,
+            )
+        );
+
+        return self::create($data);
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The language pack '{$this->other['langcode']}' was installed.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('langpackinstalledevent', 'tool_langimport');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/langimport/');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['langcode'])) {
+            throw new \coding_exception('The \'langcode\' value must be set');
+        }
+        // We can't use PARAM_LANG here as the string manager might not be aware of langpack yet.
+        $cleanedlang = clean_param($this->other['langcode'], PARAM_SAFEDIR);
+        if ($cleanedlang !== $this->other['langcode']) {
+            throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+        }
+    }
+}
diff --git a/admin/tool/langimport/classes/event/langpack_removed.php b/admin/tool/langimport/classes/event/langpack_removed.php
new file mode 100644 (file)
index 0000000..82e5270
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * The langimport langpack removed event.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack removed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string langcode: the langpage pack code.
+ * }
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_removed extends \core\event\base {
+    /**
+     * Create instance of event.
+     *
+     * @param string $langcode
+     * @return langpack_updated
+     */
+    public static function event_with_langcode($langcode) {
+        $data = array(
+            'context' => \context_system::instance(),
+            'other' => array(
+                'langcode' => $langcode,
+            )
+        );
+
+        return self::create($data);
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The language pack '{$this->other['langcode']}' was removed.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/langimport/');
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('langpackremovedevent', 'tool_langimport');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['langcode'])) {
+            throw new \coding_exception('The \'langcode\' value must be set');
+        }
+
+        // We can't use PARAM_LANG here as it queries installed strings.
+        $cleanedlang = clean_param($this->other['langcode'], PARAM_SAFEDIR);
+        if ($cleanedlang !== $this->other['langcode']) {
+            throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+        }
+    }
+}
diff --git a/admin/tool/langimport/classes/event/langpack_updated.php b/admin/tool/langimport/classes/event/langpack_updated.php
new file mode 100644 (file)
index 0000000..4f0528f
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * The langimport langpack updated event.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack updated event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string langcode: the langpage pack code.
+ * }
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_updated extends \core\event\base {
+    /**
+     * Create instance of event.
+     *
+     * @param string $langcode
+     * @return langpack_updated
+     */
+    public static function event_with_langcode($langcode) {
+        $data = array(
+            'context' => \context_system::instance(),
+            'other' => array(
+                'langcode' => $langcode,
+            )
+        );
+
+        return self::create($data);
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The language pack '{$this->other['langcode']}' was updated.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('langpackupdatedevent', 'tool_langimport');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/langimport/');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['langcode'])) {
+            throw new \coding_exception('The \'langcode\' value must be set');
+        }
+
+        $cleanedlang = clean_param($this->other['langcode'], PARAM_LANG);
+        if ($cleanedlang !== $this->other['langcode']) {
+            throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+        }
+    }
+}
diff --git a/admin/tool/langimport/classes/task/update_langpacks_task.php b/admin/tool/langimport/classes/task/update_langpacks_task.php
new file mode 100644 (file)
index 0000000..68b9e32
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A scheduled task for updating langpacks.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_langimport\task;
+
+/**
+ * A scheduled task for updating langpacks.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update_langpacks_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('updatelangs', 'tool_langimport');
+    }
+
+    /**
+     * Run langpack update
+     */
+    public function execute() {
+        $controller = new \tool_langimport\controller();
+        if ($controller->update_all_installed_languages()) {
+            foreach ($controller->info as $message) {
+                mtrace($message);
+            }
+            return true;
+        } else {
+            foreach ($controller->errors as $message) {
+                mtrace($message);
+            }
+            return false;
+        }
+
+    }
+
+}
diff --git a/admin/tool/langimport/db/tasks.php b/admin/tool/langimport/db/tasks.php
new file mode 100644 (file)
index 0000000..bd79f1b
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Definition of langimport tasks
+ *
+ * @package   tool_langimport
+ * @category  task
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => 'tool_langimport\task\update_langpacks_task',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => '4',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*'
+    )
+);
index 01d7a5d..b49dbd3 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require(dirname(dirname(dirname(dirname(__FILE__)))) . '/config.php');
+require(__DIR__.'/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/filelib.php');
-require_once($CFG->libdir.'/componentlib.class.php');
 
 admin_externalpage_setup('toollangimport');
 
@@ -65,37 +63,17 @@ define('UPDATE_ALL_LANG', 5);
 
 get_string_manager()->reset_caches();
 
-$notice_ok    = array();
-$notice_error = array();
+$controller = new tool_langimport\controller();
 
 if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($pack)) {
     core_php_time_limit::raise();
-    make_temp_directory('');
-    make_upload_directory('lang');
-
-    $installer = new lang_installer($pack);
-    $results = $installer->run();
-    foreach ($results as $langcode => $langstatus) {
-        switch ($langstatus) {
-        case lang_installer::RESULT_DOWNLOADERROR:
-            $a       = new stdClass();
-            $a->url  = $installer->lang_pack_url($langcode);
-            $a->dest = $CFG->dataroot.'/lang';
-            print_error('remotedownloaderror', 'error', 'index.php', $a);
-            break;
-        case lang_installer::RESULT_INSTALLED:
-            $notice_ok[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
-            break;
-        case lang_installer::RESULT_UPTODATE:
-            $notice_ok[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
-            break;
-        }
-    }
+    $controller->install_languagepacks($pack);
 }
 
 if ($mode == DELETION_OF_SELECTED_LANG and !empty($uninstalllang)) {
     if ($uninstalllang == 'en') {
-        $notice_error[] = 'English language pack can not be uninstalled';
+        // TODO.
+        $controller->errors[] = 'English language pack can not be uninstalled';
 
     } else if (!$confirm and confirm_sesskey()) {
         echo $OUTPUT->header();
@@ -106,120 +84,13 @@ if ($mode == DELETION_OF_SELECTED_LANG and !empty($uninstalllang)) {
         die;
 
     } else if (confirm_sesskey()) {
-        $dest1 = $CFG->dataroot.'/lang/'.$uninstalllang;
-        $dest2 = $CFG->dirroot.'/lang/'.$uninstalllang;
-        $rm1 = false;
-        $rm2 = false;
-        if (file_exists($dest1)){
-            $rm1 = remove_dir($dest1);
-        }
-        if (file_exists($dest2)){
-            $rm2 = remove_dir($dest2);
-        }
-        if ($rm1 or $rm2) {
-            $notice_ok[] = get_string('langpackremoved', 'tool_langimport');
-        } else {    //nothing deleted, possibly due to permission error
-            $notice_error[] = 'An error has occurred, language pack is not completely uninstalled, please check file permissions';
-        }
+        $controller->uninstall_language($uninstalllang);
     }
 }
 
 if ($mode == UPDATE_ALL_LANG) {
     core_php_time_limit::raise();
-
-    $installer = new lang_installer();
-
-    if (!$availablelangs = $installer->get_remote_list_of_languages()) {
-        print_error('cannotdownloadlanguageupdatelist', 'error');
-    }
-    $md5array = array();    // (string)langcode => (string)md5
-    foreach ($availablelangs as $alang) {
-        $md5array[$alang[0]] = $alang[1];
-    }
-
-    // filter out unofficial packs
-    $currentlangs = array_keys(get_string_manager()->get_list_of_translations(true));
-    $updateablelangs = array();
-    foreach ($currentlangs as $clang) {
-        if (!array_key_exists($clang, $md5array)) {
-            $notice_ok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
-            continue;
-        }
-        $dest1 = $CFG->dataroot.'/lang/'.$clang;
-        $dest2 = $CFG->dirroot.'/lang/'.$clang;
-
-        if (file_exists($dest1.'/langconfig.php') || file_exists($dest2.'/langconfig.php')){
-            $updateablelangs[] = $clang;
-        }
-    }
-
-    // then filter out packs that have the same md5 key
-    $neededlangs = array();   // all the packs that needs updating
-    foreach ($updateablelangs as $ulang) {
-        if (!is_installed_lang($ulang, $md5array[$ulang])) {
-            $neededlangs[] = $ulang;
-        }
-    }
-
-    make_temp_directory('');
-    make_upload_directory('lang');
-
-    // clean-up currently installed versions of the packs
-    foreach ($neededlangs as $packindex => $pack) {
-        if ($pack == 'en') {
-            continue;
-        }
-
-        // delete old directories
-        $dest1 = $CFG->dataroot.'/lang/'.$pack;
-        $dest2 = $CFG->dirroot.'/lang/'.$pack;
-        $rm1 = false;
-        $rm2 = false;
-        if (file_exists($dest1)) {
-            if (!remove_dir($dest1)) {
-                $notice_error[] = 'Could not delete old directory '.$dest1.', update of '.$pack.' failed, please check permissions.';
-                unset($neededlangs[$packindex]);
-                continue;
-            }
-        }
-        if (file_exists($dest2)) {
-            if (!remove_dir($dest2)) {
-                $notice_error[] = 'Could not delete old directory '.$dest2.', update of '.$pack.' failed, please check permissions.';
-                unset($neededlangs[$packindex]);
-                continue;
-            }
-        }
-    }
-
-    // install all needed language packs
-    $installer->set_queue($neededlangs);
-    $results = $installer->run();
-    $updated = false;    // any packs updated?
-    foreach ($results as $langcode => $langstatus) {
-        switch ($langstatus) {
-        case lang_installer::RESULT_DOWNLOADERROR:
-            $a       = new stdClass();
-            $a->url  = $installer->lang_pack_url($langcode);
-            $a->dest = $CFG->dataroot.'/lang';
-            print_error('remotedownloaderror', 'error', 'index.php', $a);
-            break;
-        case lang_installer::RESULT_INSTALLED:
-            $updated = true;
-            $notice_ok[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
-            break;
-        case lang_installer::RESULT_UPTODATE:
-            $notice_ok[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
-            break;
-        }
-    }
-
-    if ($updated) {
-        $notice_ok[] = get_string('langupdatecomplete', 'tool_langimport');
-    } else {
-        $notice_ok[] = get_string('nolangupdateneeded', 'tool_langimport');
-    }
-
-    unset($installer);
+    $controller->update_all_installed_languages();
 }
 get_string_manager()->reset_caches();
 
@@ -239,9 +110,7 @@ foreach ($installedlangs as $installedlang => $unused) {
     }
 }
 
-$installer = new lang_installer();
-
-if ($availablelangs = $installer->get_remote_list_of_languages()) {
+if ($availablelangs = $controller->availablelangs) {
     $remote = true;
 } else {
     $remote = false;
@@ -251,18 +120,18 @@ if ($availablelangs = $installer->get_remote_list_of_languages()) {
     echo $OUTPUT->box_end();
 }
 
-if ($notice_ok) {
-    $info = implode('<br />', $notice_ok);
+if ($controller->info) {
+    $info = implode('<br />', $controller->info);
     echo $OUTPUT->notification($info, 'notifysuccess');
 }
 
-if ($notice_error) {
-    $info = implode('<br />', $notice_error);
+if ($controller->errors) {
+    $info = implode('<br />', $controller->errors);
     echo $OUTPUT->notification($info, 'notifyproblem');
 }
 
 if ($missingparents) {
-    foreach ($missingparents as $l=>$parent) {
+    foreach ($missingparents as $l => $parent) {
         $a = new stdClass();
         $a->lang   = $installedlangs[$l];
         $a->parent = $parent;
@@ -306,7 +175,7 @@ echo html_writer::end_tag('td');
 // list of available languages
 $options = array();
 foreach ($availablelangs as $alang) {
-    if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !is_installed_lang($alang[0], $alang[1])) {
+    if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !$controller->is_installed_lang($alang[0], $alang[1])) {
         $options[$alang[0]] = $alang[2].' ('.$alang[0].')';
     }
 }
@@ -331,23 +200,3 @@ echo html_writer::end_tag('table');
 echo $OUTPUT->box_end();
 echo $OUTPUT->footer();
 die();
-
-////////////////////////////////////////////////////////////////////////////////
-// Local functions /////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
-
-/**
- * checks the md5 of the zip file, grabbed from download.moodle.org,
- * against the md5 of the local language file from last update
- * @param string $lang
- * @param string $md5check
- * @return bool
- */
-function is_installed_lang($lang, $md5check) {
-    global $CFG;
-    $md5file = $CFG->dataroot.'/lang/'.$lang.'/'.$lang.'.md5';
-    if (file_exists($md5file)){
-        return (file_get_contents($md5file) == $md5check);
-    }
-    return false;
-}
index d7534fb..fba12f9 100644 (file)
@@ -27,10 +27,14 @@ $string['install'] = 'Install selected language pack(s)';
 $string['installedlangs'] = 'Installed language packs';
 $string['langimport'] = 'Language import utility';
 $string['langimportdisabled'] = 'Language import feature has been disabled. You have to update your language packs manually at the file-system level. Do not forget to purge string caches after you do so.';
-$string['langpackinstalled'] = 'Language pack {$a} was successfully installed';
-$string['langpackremoved'] = 'Language pack was uninstalled';
-$string['langpackupdateskipped'] = 'Update of {$a} language pack skipped';
-$string['langpackuptodate'] = 'Language pack {$a} is up-to-date';
+$string['langpackinstalled'] = 'Language pack \'{$a}\' was successfully installed';
+$string['langpackinstalledevent'] = 'Language pack installed';
+$string['langpackremoved'] = 'Language pack \'{$a}\' was uninstalled';
+$string['langpackremovedevent'] = 'Language pack uninstalled';
+$string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
+$string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
+$string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
+$string['langpackupdatedevent'] = 'Language pack updated';
 $string['langupdatecomplete'] = 'Language pack update completed';
 $string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
 $string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
diff --git a/admin/tool/langimport/tests/behat/behat_tool_langimport.php b/admin/tool/langimport/tests/behat/behat_tool_langimport.php
new file mode 100644 (file)
index 0000000..f2b5729
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * Behat steps definitions for Language import tool
+ *
+ * @package   tool_langimport
+ * @category  test
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Moodle\BehatExtension\Exception\SkippedException;
+
+/**
+ * Steps definitions related with the Language import tool
+ *
+ * @package   tool_langimport
+ * @category  test
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_langimport extends behat_base {
+
+    /**
+     * This step looks to see if the remote language import tests should be run (indicated by
+     * setting TOOL_LANGIMPORT_REMOTE_TESTS in config.php.
+     *
+     * @Given /^remote langimport tests are enabled$/
+     */
+    public function remote_langimport_tests_are_enabled() {
+        if (!defined('TOOL_LANGIMPORT_REMOTE_TESTS')) {
+            throw new SkippedException('To run the remote langimport tests you must '.
+                'define TOOL_LANGIMPORT_REMOTE_TESTS in config.php');
+        }
+    }
+
+    /**
+     * Downloads a langpack and fakes it being outdated
+     *
+     * @param string $langcode The language code (e.g. en)
+     * @Given /^outdated langpack \'([^\']*)\' is installed$/
+     */
+    public function outdated_langpack_is_installed($langcode) {
+        global $CFG;
+        require_once($CFG->libdir.'/componentlib.class.php');
+
+        // Download the langpack.
+        $dir = make_upload_directory('lang');
+        $installer = new lang_installer($langcode);
+        $result = $installer->run();
+
+        if ($result[$langcode] !== lang_installer::RESULT_INSTALLED) {
+            throw new coding_exception("Failed to install langpack '$langcode'");
+        }
+
+        $path = "$dir/$langcode/$langcode.md5";
+
+        if (!file_exists($path)) {
+            throw new coding_exception("Failed to find '$langcode' checksum");
+        }
+        file_put_contents($path, '000000');
+    }
+}
diff --git a/admin/tool/langimport/tests/behat/manage_langpacks.feature b/admin/tool/langimport/tests/behat/manage_langpacks.feature
new file mode 100644 (file)
index 0000000..ff21c8e
--- /dev/null
@@ -0,0 +1,59 @@
+@tool @tool_langimport
+Feature: Manage language packs
+  In order to support different languages
+  As an administrator
+  I need to be able to add, update and remove language packs
+
+
+  Background:
+    Given remote langimport tests are enabled
+
+  # The pirate language pack is used for testing because its small to download.
+
+  Scenario: Install language pack
+    Given I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    When I set the field "Available language packs" to "English - Pirate (en_ar)"
+    And I press "Install selected language pack(s)"
+    Then I should see "Language pack 'en_ar' was successfully installed"
+    And the "Installed language packs" select box should contain "English - Pirate (en_ar)"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should see "The language pack 'en_ar' was installed."
+    And I log out
+
+  Scenario: Update language pack
+    Given outdated langpack 'en_ar' is installed
+    And I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    When I press "Update all installed language packs"
+    Then I should see "Language pack 'en_ar' was successfully updated"
+    And I should see "Language pack update completed"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should see "The language pack 'en_ar' was updated."
+    And I log out
+
+  Scenario: Try to uninstall language pack
+    Given I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    And I set the field "Available language packs" to "English - Pirate (en_ar)"
+    And I press "Install selected language pack(s)"
+    When I set the field "Installed language packs" to "English - Pirate (en_ar)"
+    And I press "Uninstall selected language pack"
+    And I press "Continue"
+    Then I should see "Language pack 'en_ar' was uninstalled"
+    And the "Installed language packs" select box should not contain "English - Pirate (en_ar)"
+    And the "Available language packs" select box should contain "English - Pirate (en_ar)"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should see "The language pack 'en_ar' was removed."
+    And I should see "Language pack uninstalled"
+    And I log out
+
+  Scenario: Try to uninstall English language pack
+    Given I log in as "admin"
+    And I navigate to "Language packs" node in "Site administration > Language"
+    When I set the field "Installed language packs" to "English (en)"
+    And I press "Uninstall selected language pack"
+    Then I should see "English language pack can not be uninstalled"
+    And I navigate to "Live logs" node in "Site administration > Reports"
+    And I should not see "Language pack uninstalled"
+    And I log out
diff --git a/admin/tool/langimport/tests/events_test.php b/admin/tool/langimport/tests/events_test.php
new file mode 100644 (file)
index 0000000..0a75b34
--- /dev/null
@@ -0,0 +1,102 @@
+<?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 langimport events.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test class for langimport events.
+ *
+ * @package    tool_langimport
+ * @copyright  2014 Dan Poltawski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+class tool_langimport_events_testcase extends advanced_testcase {
+
+    /**
+     * Setup testcase.
+     */
+    public function setUp() {
+        $this->setAdminUser();
+        $this->resetAfterTest();
+    }
+
+    public function test_langpack_updated() {
+        global $CFG;
+
+        $event = \tool_langimport\event\langpack_updated::event_with_langcode($CFG->lang);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\tool_langimport\event\langpack_updated', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    public function test_langpack_updated_validation() {
+        $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+        \tool_langimport\event\langpack_updated::event_with_langcode('broken langcode');
+    }
+
+    public function test_langpack_installed() {
+        $event = \tool_langimport\event\langpack_imported::event_with_langcode('fr');
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\tool_langimport\event\langpack_imported', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    public function test_langpack_installed_validation() {
+        $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+        \tool_langimport\event\langpack_imported::event_with_langcode('broken langcode');
+    }
+
+    public function test_langpack_removed() {
+        $event = \tool_langimport\event\langpack_removed::event_with_langcode('fr');
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\tool_langimport\event\langpack_removed', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+    }
+
+    public function test_langpack_removed_validation() {
+        $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+        \tool_langimport\event\langpack_removed::event_with_langcode('broken langcode');
+    }
+}
index 398cfc3..7050367 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2014092801; // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800; // Requires this Moodle version
 $plugin->component = 'tool_langimport'; // Full name of the plugin (used for diagnostics)
diff --git a/admin/tool/messageinbound/classes/edit_handler_form.php b/admin/tool/messageinbound/classes/edit_handler_form.php
new file mode 100644 (file)
index 0000000..bcb6af5
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form to edit handlers.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form to edit handlers.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_messageinbound_edit_handler_form extends moodleform {
+    public function definition() {
+        $mform = $this->_form;
+
+        $handler = $this->_customdata['handler'];
+
+        // Set up the options for formatting text for descriptions, etc.
+        $formatoptions = new stdClass();
+        $formatoptions->trusted = false;
+        $formatoptions->noclean = false;
+        $formatoptions->smiley = false;
+        $formatoptions->filter = false;
+        $formatoptions->para = true;
+        $formatoptions->newlines = false;
+        $formatoptions->overflowdiv = true;
+
+        // General information about the handler.
+        $mform->addElement('header', 'general', get_string('general'));
+        $mform->addElement('static', 'name', get_string('name', 'tool_messageinbound'),
+            $handler->name);
+        $mform->addElement('static', 'classname', get_string('classname', 'tool_messageinbound'));
+
+        $description = format_text($handler->description, FORMAT_MARKDOWN, $formatoptions);
+
+        $mform->addElement('static', 'description', get_string('description', 'tool_messageinbound'),
+            $description);
+
+        // Items which can be configured.
+        $mform->addElement('header', 'configuration', get_string('configuration'));
+
+        $options = array(
+            HOURSECS => get_string('onehour', 'tool_messageinbound'),
+            DAYSECS => get_string('oneday', 'tool_messageinbound'),
+            WEEKSECS => get_string('oneweek', 'tool_messageinbound'),
+            YEARSECS => get_string('oneyear', 'tool_messageinbound'),
+            '' => get_string('noexpiry', 'tool_messageinbound'),
+        );
+        $mform->addElement('select', 'defaultexpiration', get_string('defaultexpiration', 'tool_messageinbound'), $options);
+        $mform->addHelpButton('defaultexpiration', 'defaultexpiration', 'tool_messageinbound');
+
+        if ($handler->can_change_validateaddress()) {
+            $mform->addElement('checkbox', 'validateaddress', get_string('requirevalidation', 'tool_messageinbound'));
+            $mform->addHelpButton('validateaddress', 'validateaddress', 'tool_messageinbound');
+        } else {
+            if ($handler->validateaddress) {
+                $text = get_string('yes');
+            } else {
+                $text = get_string('no');
+            }
+            $mform->addElement('static', 'validateaddress_fake', get_string('requirevalidation', 'tool_messageinbound'), $text);
+            $mform->addElement('hidden', 'validateaddress');
+            $mform->addHelpButton('validateaddress_fake', 'fixedvalidateaddress', 'tool_messageinbound');
+            $mform->setType('validateaddress', PARAM_INT);
+        }
+
+        if ($handler->can_change_enabled()) {
+            $mform->addElement('checkbox', 'enabled', get_string('enabled', 'tool_messageinbound'));
+        } else {
+            if ($handler->enabled) {
+                $text = get_string('yes');
+            } else {
+                $text = get_string('no');
+            }
+            $mform->addElement('static', 'enabled_fake', get_string('enabled', 'tool_messageinbound'), $text);
+            $mform->addHelpButton('enabled', 'fixedenabled', 'tool_messageinbound');
+            $mform->addElement('hidden', 'enabled');
+            $mform->setType('enabled', PARAM_INT);
+        }
+
+        $this->add_action_buttons(true, get_string('savechanges'));
+    }
+}
diff --git a/admin/tool/messageinbound/classes/manager.php b/admin/tool/messageinbound/classes/manager.php
new file mode 100644 (file)
index 0000000..ebfa8ab
--- /dev/null
@@ -0,0 +1,953 @@
+<?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/>.
+
+/**
+ * The Mail Pickup Manager.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Mail Pickup Manager.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+    /**
+     * @var string The main mailbox to check.
+     */
+    const MAILBOX = 'INBOX';
+
+    /**
+     * @var string The mailbox to store messages in when they are awaiting confirmation.
+     */
+    const CONFIRMATIONFOLDER = 'tobeconfirmed';
+
+    /**
+     * @var string The flag for seen/read messages.
+     */
+    const MESSAGE_SEEN = '\seen';
+
+    /**
+     * @var string The flag for flagged messages.
+     */
+    const MESSAGE_FLAGGED = '\flagged';
+
+    /**
+     * @var string The flag for deleted messages.
+     */
+    const MESSAGE_DELETED = '\deleted';
+
+    /**
+     * @var Horde_Imap_Client_Socket A reference to the IMAP client.
+     */
+    protected $client = null;
+
+    /**
+     * @var \core\message\inbound\address_manager A reference to the Inbound Message Address Manager instance.
+     */
+    protected $addressmanager = null;
+
+    /**
+     * @var stdClass The data for the current message being processed.
+     */
+    protected $currentmessagedata = null;
+
+    /**
+     * Retrieve the connection to the IMAP client.
+     *
+     * @return bool Whether a connection was successfully established.
+     */
+    protected function get_imap_client() {
+        global $CFG;
+
+        if (!\core\message\inbound\manager::is_enabled()) {
+            // E-mail processing not set up.
+            mtrace("Inbound Message not fully configured - exiting early.");
+            return false;
+        }
+
+        mtrace("Connecting to {$CFG->messageinbound_host} as {$CFG->messageinbound_hostuser}...");
+
+        $configuration = array(
+            'username' => $CFG->messageinbound_hostuser,
+            'password' => $CFG->messageinbound_hostpass,
+            'hostspec' => $CFG->messageinbound_host,
+            'secure'   => $CFG->messageinbound_hostssl,
+        );
+
+        $this->client = new \Horde_Imap_Client_Socket($configuration);
+
+        try {
+            $this->client->login();
+            mtrace("Connection established.");
+            return true;
+
+        } catch (\Horde_Imap_Client_Exception $e) {
+            $message = $e->getMessage();
+            mtrace("Unable to connect to IMAP server. Failed with '{$message}'");
+
+            return false;
+        }
+    }
+
+    /**
+     * Shutdown and close the connection to the IMAP client.
+     */
+    protected function close_connection() {
+        if ($this->client) {
+            $this->client->close();
+        }
+        $this->client = null;
+    }
+
+    /**
+     * Get the current mailbox information.
+     *
+     * @return \Horde_Imap_Client_Mailbox
+     */
+    protected function get_mailbox() {
+        // Get the current mailbox.
+        $mailbox = $this->client->currentMailbox();
+
+        if (isset($mailbox['mailbox'])) {
+            return $mailbox['mailbox'];
+        } else {
+            throw new \core\message\inbound\processing_failed_exception('couldnotopenmailbox', 'tool_messageinbound');
+        }
+    }
+
+    /**
+     * Execute the main Inbound Message pickup task.
+     */
+    public function pickup_messages() {
+        if (!$this->get_imap_client()) {
+            return false;
+        }
+
+        // Restrict results to messages which are unseen, and have not been flagged.
+        $search = new \Horde_Imap_Client_Search_Query();
+        $search->flag(self::MESSAGE_SEEN, false);
+        $search->flag(self::MESSAGE_FLAGGED, false);
+        mtrace("Searching for Unseen, Unflagged email in the folder '" . self::MAILBOX . "'");
+        $results = $this->client->search(self::MAILBOX, $search);
+
+        // We require the envelope data and structure of each message.
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->envelope();
+        $query->structure();
+
+        // Retrieve the message id.
+        $messages = $this->client->fetch(self::MAILBOX, $query, array('ids' => $results['match']));
+
+        mtrace("Found " . $messages->count() . " messages to parse. Parsing...");
+        $this->addressmanager = new \core\message\inbound\address_manager();
+        foreach ($messages as $message) {
+            $this->process_message($message);
+        }
+
+        // Close the client connection.
+        $this->close_connection();
+
+        return true;
+    }
+
+    /**
+     * Process a message received and validated by the Inbound Message processor.
+     *
+     * @param stdClass $maildata The data retrieved from the database for the current record.
+     * @return bool Whether the message was successfully processed.
+     */
+    public function process_existing_message(\stdClass $maildata) {
+        // Grab the new IMAP client.
+        if (!$this->get_imap_client()) {
+            return false;
+        }
+
+        // Build the search.
+        $search = new \Horde_Imap_Client_Search_Query();
+        // When dealing with Inbound Message messages, we mark them as flagged and seen. Restrict the search to those criterion.
+        $search->flag(self::MESSAGE_SEEN, true);
+        $search->flag(self::MESSAGE_FLAGGED, true);
+        mtrace("Searching for a Seen, Flagged message in the folder '" . self::CONFIRMATIONFOLDER . "'");
+
+        // Match the message ID.
+        $search->headerText('message-id', $maildata->messageid);
+        $search->headerText('to', $maildata->address);
+
+        $results = $this->client->search(self::CONFIRMATIONFOLDER, $search);
+
+        // Build the base query.
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->envelope();
+        $query->structure();
+
+
+        // Fetch the first message from the client.
+        $messages = $this->client->fetch(self::CONFIRMATIONFOLDER, $query, array('ids' => $results['match']));
+        $this->addressmanager = new \core\message\inbound\address_manager();
+        if ($message = $messages->first()) {
+            mtrace("--> Found the message. Passing back to the pickup system.");
+
+            // Process the message.
+            $this->process_message($message, true, true);
+
+            // Close the client connection.
+            $this->close_connection();
+
+            mtrace("============================================================================");
+            return true;
+        } else {
+            // Close the client connection.
+            $this->close_connection();
+
+            mtrace("============================================================================");
+            throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
+        }
+    }
+
+    /**
+     * Tidy up old messages in the confirmation folder.
+     *
+     * @return bool Whether tidying occurred successfully.
+     */
+    public function tidy_old_messages() {
+        // Grab the new IMAP client.
+        if (!$this->get_imap_client()) {
+            return false;
+        }
+
+        // Open the mailbox.
+        mtrace("Searching for messages older than 24 hours in the '" .
+                self::CONFIRMATIONFOLDER . "' folder.");
+        $this->client->openMailbox(self::CONFIRMATIONFOLDER);
+
+        $mailbox = $this->get_mailbox();
+
+        // Build the search.
+        $search = new \Horde_Imap_Client_Search_Query();
+
+        // Delete messages older than 24 hours old.
+        $search->intervalSearch(DAYSECS, \Horde_Imap_Client_Search_Query::INTERVAL_OLDER);
+
+        $results = $this->client->search($mailbox, $search);
+
+        // Build the base query.
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->envelope();
+
+        // Retrieve the messages and mark them for removal.
+        $messages = $this->client->fetch($mailbox, $query, array('ids' => $results['match']));
+        mtrace("Found " . $messages->count() . " messages for removal.");
+        foreach ($messages as $message) {
+            $this->add_flag_to_message($message->getUid(), self::MESSAGE_DELETED);
+        }
+
+        mtrace("Finished removing messages.");
+        $this->close_connection();
+
+        return true;
+    }
+
+    /**
+     * Process a message and pass it through the Inbound Message handling systems.
+     *
+     * @param Horde_Imap_Client_Data_Fetch $message The message to process
+     * @param bool $viewreadmessages Whether to also look at messages which have been marked as read
+     * @param bool $skipsenderverification Whether to skip the sender verificiation stage
+     */
+    public function process_message(
+            \Horde_Imap_Client_Data_Fetch $message,
+            $viewreadmessages = false,
+            $skipsenderverification = false) {
+        global $USER;
+
+        // We use the Client IDs several times - store them here.
+        $messageid = new \Horde_Imap_Client_Ids($message->getUid());
+
+        mtrace("- Parsing message " . $messageid);
+
+        // First flag this message to prevent another running hitting this message while we look at the headers.
+        $this->add_flag_to_message($messageid, self::MESSAGE_FLAGGED);
+
+        // Record the user that this script is currently being run as.  This is important when re-processing existing
+        // messages, as cron_setup_user is called multiple times.
+        $originaluser = $USER;
+
+        $envelope = $message->getEnvelope();
+        $recipients = $envelope->to->bare_addresses;
+        foreach ($recipients as $recipient) {
+            if (!\core\message\inbound\address_manager::is_correct_format($recipient)) {
+                // Message did not contain a subaddress.
+                mtrace("- Recipient '{$recipient}' did not match Inbound Message headers.");
+                continue;
+            }
+
+            // Message contained a match.
+            $senders = $message->getEnvelope()->from->bare_addresses;
+            if (count($senders) !== 1) {
+                mtrace("- Received multiple senders. Only the first sender will be used.");
+            }
+            $sender = array_shift($senders);
+
+            mtrace("-- Subject:\t"      . $envelope->subject);
+            mtrace("-- From:\t"         . $sender);
+            mtrace("-- Recipient:\t"    . $recipient);
+
+            // Grab messagedata including flags.
+            $query = new \Horde_Imap_Client_Fetch_Query();
+            $query->structure();
+            $messagedata = $this->client->fetch($this->get_mailbox(), $query, array(
+                'ids' => $messageid,
+            ))->first();
+
+            if (!$viewreadmessages && $this->message_has_flag($messageid, self::MESSAGE_SEEN)) {
+                // Something else has already seen this message. Skip it now.
+                mtrace("-- Skipping the message - it has been marked as seen - perhaps by another process.");
+                continue;
+            }
+
+            // Mark it as read to lock the message.
+            $this->add_flag_to_message($messageid, self::MESSAGE_SEEN);
+
+            // Now pass it through the Inbound Message processor.
+            $status = $this->addressmanager->process_envelope($recipient, $sender);
+
+            // Check the validation status early. No point processing garbage messages, but we do need to process it
+            // for some validation failure types.
+            if (!$this->passes_key_validation($status, $messageid)) {
+                // None of the above validation failures were found. Skip this message.
+                mtrace("-- Skipped message - it does not appear to relate to a Inbound Message pickup. Fail code {$status}");
+
+                // Remove the seen flag from the message as there may be multiple recipients.
+                $this->remove_flag_from_message($messageid, self::MESSAGE_SEEN);
+
+                // Skip further processing for this recipient.
+                continue;
+            }
+
+            // Process the message as the user.
+            $user = $this->addressmanager->get_data()->user;
+            mtrace("-- Processing the message as user {$user->id} ({$user->username}).");
+            cron_setup_user($user);
+
+            // Process and retrieve the message data for this message.
+            // This includes fetching the full content, as well as all headers, and attachments.
+            $this->process_message_data($envelope, $messagedata, $messageid);
+
+            // When processing validation replies, we need to skip the sender verification phase as this has been
+            // manually completed.
+            if (!$skipsenderverification && $status !== 0) {
+                // Check the validation status for failure types which require confirmation.
+                // The validation result is tested in a bitwise operation.
+                mtrace("-- Message did not meet validation but is possibly recoverable. Fail code {$status}");
+                // This is a recoverable error, but requires user input.
+
+                if ($this->handle_verification_failure($messageid, $recipient)) {
+                    mtrace("--- Original message retained on mail server and confirmation message sent to user.");
+                } else {
+                    mtrace("--- Invalid Recipient Handler - unable to save. Informing the user of the failure.");
+                    $this->inform_user_of_error(get_string('invalidrecipientfinal', 'tool_messageinbound', $this->currentmessagedata));
+                }
+
+                // Returning to normal cron user.
+                mtrace("-- Returning to the original user.");
+                cron_setup_user($originaluser);
+                return;
+            }
+
+            // Add the content and attachment data.
+            mtrace("-- Validation completed. Fetching rest of message content.");
+            $this->process_message_data_body($messagedata, $messageid);
+
+            // The message processor throws exceptions upon failure. These must be caught and notifications sent to
+            // the user here.
+            try {
+                $result = $this->send_to_handler();
+            } catch (\core\message\inbound\processing_failed_exception $e) {
+                // We know about these kinds of errors and they should result in the user being notified of the
+                // failure. Send the user a notification here.
+                $this->inform_user_of_error($e->getMessage());
+
+                // Returning to normal cron user.
+                mtrace("-- Returning to the original user.");
+                cron_setup_user($originaluser);
+                return;
+            } catch (Exception $e) {
+                // An unknown error occurred. The user is not informed, but the administrator is.
+                mtrace("-- Message processing failed. An unexpected exception was thrown. Details follow.");
+                mtrace($e->getMessage());
+
+                // Returning to normal cron user.
+                mtrace("-- Returning to the original user.");
+                cron_setup_user($originaluser);
+                return;
+            }
+
+            if ($result) {
+                // Handle message cleanup. Messages are deleted once fully processed.
+                mtrace("-- Marking the message for removal.");
+                $this->add_flag_to_message($messageid, self::MESSAGE_DELETED);
+            } else {
+                mtrace("-- The Inbound Message processor did not return a success status. Skipping message removal.");
+            }
+
+            // Returning to normal cron user.
+            mtrace("-- Returning to the original user.");
+            cron_setup_user($originaluser);
+
+            mtrace("-- Finished processing " . $message->getUid());
+
+            // Skip the outer loop too. The message has already been processed and it could be possible for there to
+            // be two recipients in the envelope which match somehow.
+            return;
+        }
+    }
+
+    /**
+     * Process a message to retrieve it's header data without body and attachemnts.
+     *
+     * @param Horde_Imap_Client_Data_Envelope $envelope The Envelope of the message
+     * @param Horde_Imap_Client_Data_Fetch $messagedata The structure and part of the message body
+     * @param string|Horde_Imap_Client_Ids $messageid The Hore message Uid
+     * @return \stdClass The current value of the messagedata
+     */
+    private function process_message_data(
+            \Horde_Imap_Client_Data_Envelope $envelope,
+            \Horde_Imap_Client_Data_Fetch $basemessagedata,
+            $messageid) {
+
+        // Get the current mailbox.
+        $mailbox = $this->get_mailbox();
+
+        // We need the structure at various points below.
+        $structure = $basemessagedata->getStructure();
+
+        // Now fetch the rest of the message content.
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->imapDate();
+
+        // Fetch all of the message parts too.
+        $typemap = $structure->contentTypeMap();
+        foreach ($typemap as $part => $type) {
+            // The header.
+            $query->headerText(array(
+                'id' => $part,
+            ));
+        }
+
+        $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first();
+
+        // Store the data for this message.
+        $headers = '';
+
+        foreach ($typemap as $part => $type) {
+            // Grab all of the header data into a string.
+            $headers .= $messagedata->getHeaderText($part);
+
+            // We don't handle any of the other MIME content at this stage.
+        }
+
+        $data = new \stdClass();
+
+        // The message ID should always be in the first part.
+        $data->messageid = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Message-ID');
+        $data->subject = $envelope->subject;
+        $data->timestamp = $messagedata->getImapDate()->__toString();
+        $data->envelope = $envelope;
+        $data->data = $this->addressmanager->get_data();
+        $data->headers = $headers;
+
+        $this->currentmessagedata = $data;
+
+        return $this->currentmessagedata;
+    }
+
+    /**
+     * Process a message again to add body and attachment data.
+     *
+     * @param Horde_Imap_Client_Data_Envelope $envelope The Envelope of the message
+     * @param Horde_Imap_Client_Data_Fetch $basemessagedata The structure and part of the message body
+     * @param string|Horde_Imap_Client_Ids $messageid The Hore message Uid
+     * @return \stdClass The current value of the messagedata
+     */
+    private function process_message_data_body(
+            \Horde_Imap_Client_Data_Fetch $basemessagedata,
+            $messageid) {
+        global $CFG;
+
+        // Get the current mailbox.
+        $mailbox = $this->get_mailbox();
+
+        // We need the structure at various points below.
+        $structure = $basemessagedata->getStructure();
+
+        // Now fetch the rest of the message content.
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->fullText();
+
+        // Fetch all of the message parts too.
+        $typemap = $structure->contentTypeMap();
+        foreach ($typemap as $part => $type) {
+            // The body of the part - attempt to decode it on the server.
+            $query->bodyPart($part, array(
+                'decode' => true,
+                'peek' => true,
+            ));
+            $query->bodyPartSize($part);
+        }
+
+        $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first();
+
+        // Store the data for this message.
+        $contentplain = '';
+        $contenthtml = '';
+        $attachments = array(
+            'inline' => array(),
+            'attachment' => array(),
+        );
+
+        $plainpartid = $structure->findBody('plain');
+        $htmlpartid = $structure->findBody('html');
+
+        foreach ($typemap as $part => $type) {
+            // Get the message data from the body part, and combine it with the structure to give a fully-formed output.
+            $stream = $messagedata->getBodyPart($part, true);
+            $partdata = $structure->getPart($part);
+            $partdata->setContents($stream, array(
+                'usestream' => true,
+            ));
+
+            if ($part === $plainpartid) {
+                $contentplain = $this->process_message_part_body($messagedata, $partdata, $part);
+
+            } else if ($part === $htmlpartid) {
+                $contenthtml = $this->process_message_part_body($messagedata, $partdata, $part);
+
+            } else if ($filename = $partdata->getName($part)) {
+                if ($attachment = $this->process_message_part_attachment($messagedata, $partdata, $part, $filename)) {
+                    // The disposition should be one of 'attachment', 'inline'.
+                    // If an empty string is provided, default to 'attachment'.
+                    $disposition = $partdata->getDisposition();
+                    $disposition = $disposition == 'inline' ? 'inline' : 'attachment';
+                    $attachments[$disposition][] = $attachment;
+                }
+            }
+
+            // We don't handle any of the other MIME content at this stage.
+        }
+
+        // The message ID should always be in the first part.
+        $this->currentmessagedata->plain = $contentplain;
+        $this->currentmessagedata->html = $contenthtml;
+        $this->currentmessagedata->attachments = $attachments;
+
+        return $this->currentmessagedata;
+    }
+
+    /**
+     * Process the messagedata and part data to extract the content of this part.
+     *
+     * @param $messagedata The structure and part of the message body
+     * @param $partdata The part data
+     * @param $part The part ID
+     * @return string
+     */
+    private function process_message_part_body($messagedata, $partdata, $part) {
+        // This is a content section for the main body.
+
+        // Get the string version of it.
+        $content = $messagedata->getBodyPart($part);
+        if (!$messagedata->getBodyPartDecode($part)) {
+            // Decode the content.
+            $partdata->setContents($content);
+            $content = $partdata->getContents();
+        }
+
+        // Convert the text from the current encoding to UTF8.
+        $content = \core_text::convert($content, $partdata->getCharset());
+
+        // Fix any invalid UTF8 characters.
+        // Note: XSS cleaning is not the responsibility of this code. It occurs immediately before display when
+        // format_text is called.
+        $content = clean_param($content, PARAM_RAW);
+
+        return $content;
+    }
+
+    /**
+     * Process a message again to add body and attachment data.
+     *
+     * @param $messagedata The structure and part of the message body
+     * @param $partdata The part data
+     * @param $filename The filename of the attachment
+     * @return \stdClass
+     */
+    private function process_message_part_attachment($messagedata, $partdata, $part, $filename) {
+        global $CFG;
+
+        // If a filename is present, assume that this part is an attachment.
+        $attachment = new \stdClass();
+        $attachment->filename       = $filename;
+        $attachment->type           = $partdata->getType();
+        $attachment->content        = $partdata->getContents();
+        $attachment->charset        = $partdata->getCharset();
+        $attachment->description    = $partdata->getDescription();
+        $attachment->contentid      = $partdata->getContentId();
+        $attachment->filesize       = $messagedata->getBodyPartSize($part);
+
+        if (empty($CFG->runclamonupload) or empty($CFG->pathtoclam)) {
+            mtrace("--> Attempting virus scan of '{$attachment->filename}'");
+
+            // Store the file on disk - it will need to be virus scanned first.
+            $itemid = rand(1, 999999999);;
+            $directory = make_temp_directory("/messageinbound/{$itemid}", false);
+            $filepath = $directory . "/" . $attachment->filename;
+            if (!$fp = fopen($filepath, "w")) {
+                // Unable to open the temporary file to write this to disk.
+                mtrace("--> Unable to save the file to disk for virus scanning. Check file permissions.");
+
+                throw new \core\message\inbound\processing_failed_exception('attachmentfilepermissionsfailed',
+                        'tool_messageinbound');
+            }
+
+            fwrite($fp, $attachment->content);
+            fclose($fp);
+
+            // Perform a virus scan now.
+            try {
+                \repository::antivir_scan_file($filepath, $attachment->filename, true);
+            } catch (moodle_exception $e) {
+                mtrace("--> A virus was found in the attachment '{$attachment->filename}'.");
+                $this->inform_attachment_virus();
+                return;
+            }
+        }
+
+        return $attachment;
+    }
+
+    /**
+     * Check whether the key provided is valid.
+     *
+     * @param $status The Message to process
+     * @param $messageid The Hore message Uid
+     * @return bool
+     */
+    private function passes_key_validation($status, $messageid) {
+        // The validation result is tested in a bitwise operation.
+        if ((
+            $status & ~ \core\message\inbound\address_manager::VALIDATION_SUCCESS
+                    & ~ \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY
+                    & ~ \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY
+                    & ~ \core\message\inbound\address_manager::VALIDATION_INVALID_HASH
+                    & ~ \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH) !== 0) {
+
+            // One of the above bits was found in the status - fail the validation.
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Add the specified flag to the message.
+     *
+     * @param $messageid
+     * @param string $flag The flag to add
+     */
+    private function add_flag_to_message($messageid, $flag) {
+        // Get the current mailbox.
+        $mailbox = $this->get_mailbox();
+
+        // Mark it as read to lock the message.
+        $this->client->store($mailbox, array(
+            'ids' => new \Horde_Imap_Client_Ids($messageid),
+            'add' => $flag,
+        ));
+    }
+
+    /**
+     * Remove the specified flag from the message.
+     *
+     * @param $messageid
+     * @param string $flag The flag to remove
+     */
+    private function remove_flag_from_message($messageid, $flag) {
+        // Get the current mailbox.
+        $mailbox = $this->get_mailbox();
+
+        // Mark it as read to lock the message.
+        $this->client->store($mailbox, array(
+            'ids' => $messageid,
+            'delete' => $flag,
+        ));
+    }
+
+    /**
+     * Check whether the message has the specified flag
+     *
+     * @param $messageid
+     * @param string $flag The flag to check
+     * @return bool
+     */
+    private function message_has_flag($messageid, $flag) {
+        // Get the current mailbox.
+        $mailbox = $this->get_mailbox();
+
+        // Grab messagedata including flags.
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->flags();
+        $query->structure();
+        $messagedata = $this->client->fetch($mailbox, $query, array(
+            'ids' => $messageid,
+        ))->first();
+        $flags = $messagedata->getFlags();
+
+        return in_array($flag, $flags);
+    }
+
+    /**
+     * Send the message to the appropriate handler.
+     *
+     */
+    private function send_to_handler() {
+        try {
+            mtrace("--> Passing to Inbound Message handler {$this->addressmanager->get_handler()->classname}");
+            if ($result = $this->addressmanager->handle_message($this->currentmessagedata)) {
+                $this->inform_user_of_success($this->currentmessagedata, $result);
+                // Request that this message be marked for deletion.
+                return true;
+            }
+
+        } catch (\core\message\inbound\processing_failed_exception $e) {
+            mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. The user has been informed.");
+            mtrace("--> " . $e->getMessage());
+            // Throw the exception again, with additional data.
+            $error = new \stdClass();
+            $error->subject     = $this->currentmessagedata->envelope->subject;
+            $error->message     = $e->getMessage();
+            throw new \core\message\inbound\processing_failed_exception('messageprocessingfailed', 'tool_messageinbound', $error);
+
+        } catch (Exception $e) {
+            mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. User informed.");
+            mtrace("--> " . $e->getMessage());
+            // An unknown error occurred. Still inform the user but, this time do not include the specific
+            // message information.
+            $error = new \stdClass();
+            $error->subject     = $this->currentmessagedata->envelope->subject;
+            throw new \core\message\inbound\processing_failed_exception('messageprocessingfailedunknown',
+                    'tool_messageinbound', $error);
+
+        }
+
+        // Something went wrong and the message was not handled well in the Inbound Message handler.
+        mtrace("-> The Inbound Message handler reported an error. The message may not have been processed.");
+
+        // It is the responsiblity of the handler to throw an appropriate exception if the message was not processed.
+        // Do not inform the user at this point.
+        return false;
+    }
+
+    /**
+     * Handle failure of sender verification.
+     *
+     * This will send a notification to the user identified in the Inbound Message address informing them that a message has been
+     * stored. The message includes a verification link and reply-to address which is handled by the
+     * invalid_recipient_handler.
+     *
+     * @param $recipient The message recipient
+     */
+    private function handle_verification_failure(
+            \Horde_Imap_Client_Ids $messageids,
+            $recipient) {
+        global $DB, $USER;
+
+        if (!$messageid = $this->currentmessagedata->messageid) {
+            mtrace("---> Warning: Unable to determine the Message-ID of the message.");
+            return false;
+        }
+
+        // Move the message into a new mailbox.
+        $this->client->copy(self::MAILBOX, self::CONFIRMATIONFOLDER, array(
+                'create'    => true,
+                'ids'       => $messageids,
+                'move'      => true,
+            ));
+
+        // Store the data from the failed message in the associated table.
+        $record = new \stdClass();
+        $record->messageid = $messageid;
+        $record->userid = $USER->id;
+        $record->address = $recipient;
+        $record->timecreated = time();
+        $record->id = $DB->insert_record('messageinbound_messagelist', $record);
+
+        // Setup the Inbound Message generator for the invalid recipient handler.
+        $addressmanager = new \core\message\inbound\address_manager();
+        $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
+        $addressmanager->set_data($record->id);
+
+        $eventdata = new \stdClass();
+        $eventdata->component           = 'tool_messageinbound';
+        $eventdata->name                = 'invalidrecipienthandler';
+
+        $userfrom = clone $USER;
+        $userfrom->customheaders = array();
+        // Adding the In-Reply-To header ensures that it is seen as a reply.
+        $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
+
+        // The message will be sent from the intended user.
+        $eventdata->userfrom            = \core_user::get_noreply_user();
+        $eventdata->userto              = $USER;
+        $eventdata->subject             = $this->get_reply_subject($this->currentmessagedata->envelope->subject);
+        $eventdata->fullmessage         = get_string('invalidrecipientdescription', 'tool_messageinbound', $this->currentmessagedata);
+        $eventdata->fullmessageformat   = FORMAT_PLAIN;
+        $eventdata->fullmessagehtml     = get_string('invalidrecipientdescriptionhtml', 'tool_messageinbound', $this->currentmessagedata);
+        $eventdata->smallmessage        = $eventdata->fullmessage;
+        $eventdata->notification        = 1;
+        $eventdata->replyto             = $addressmanager->generate($USER->id);
+
+        mtrace("--> Sending a message to the user to report an verification failure.");
+        if (!message_send($eventdata)) {
+            mtrace("---> Warning: Message could not be sent.");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Inform the identified sender of a processing error.
+     *
+     * @param string $error The error message
+     */
+    private function inform_user_of_error($error) {
+        global $USER;
+
+        // The message will be sent from the intended user.
+        $userfrom = clone $USER;
+        $userfrom->customheaders = array();
+
+        if ($messageid = $this->currentmessagedata->messageid) {
+            // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained.
+            $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
+        }
+
+        $messagedata = new \stdClass();
+        $messagedata->subject = $this->currentmessagedata->envelope->subject;
+        $messagedata->error = $error;
+
+        $eventdata = new \stdClass();
+        $eventdata->component           = 'tool_messageinbound';
+        $eventdata->name                = 'messageprocessingerror';
+        $eventdata->userfrom            = $userfrom;
+        $eventdata->userto              = $USER;
+        $eventdata->subject             = self::get_reply_subject($this->currentmessagedata->envelope->subject);
+        $eventdata->fullmessage         = get_string('messageprocessingerror', 'tool_messageinbound', $messagedata);
+        $eventdata->fullmessageformat   = FORMAT_PLAIN;
+        $eventdata->fullmessagehtml     = get_string('messageprocessingerrorhtml', 'tool_messageinbound', $messagedata);
+        $eventdata->smallmessage        = $eventdata->fullmessage;
+        $eventdata->notification        = 1;
+
+        if (message_send($eventdata)) {
+            mtrace("---> Notification sent to {$USER->email}.");
+        } else {
+            mtrace("---> Unable to send notification.");
+        }
+    }
+
+    /**
+     * Inform the identified sender that message processing was successful.
+     *
+     * @param stdClass $messagedata The data for the current message being processed.
+     * @param mixed $handlerresult The result returned by the handler.
+     */
+    private function inform_user_of_success(\stdClass $messagedata, $handlerresult) {
+        global $USER;
+
+        // Check whether the handler has a success notification.
+        $handler = $this->addressmanager->get_handler();
+        $message = $handler->get_success_message($messagedata, $handlerresult);
+
+        if (!$message) {
+            mtrace("---> Handler has not defined a success notification e-mail.");
+            return false;
+        }
+
+        // Wrap the message in the notification wrapper.
+        $messageparams = new \stdClass();
+        $messageparams->html    = $message->html;
+        $messageparams->plain   = $message->plain;
+        $messagepreferencesurl = new \moodle_url("/message/edit.php", array('id' => $USER->id));
+        $messageparams->messagepreferencesurl = $messagepreferencesurl->out();
+        $htmlmessage = get_string('messageprocessingsuccesshtml', 'tool_messageinbound', $messageparams);
+        $plainmessage = get_string('messageprocessingsuccess', 'tool_messageinbound', $messageparams);
+
+        // The message will be sent from the intended user.
+        $userfrom = clone $USER;
+        $userfrom->customheaders = array();
+
+        if ($messageid = $this->currentmessagedata->messageid) {
+            // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained.
+            $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
+        }
+
+        $messagedata = new \stdClass();
+        $messagedata->subject = $this->currentmessagedata->envelope->subject;
+
+        $eventdata = new \stdClass();
+        $eventdata->component           = 'tool_messageinbound';
+        $eventdata->name                = 'messageprocessingsuccess';
+        $eventdata->userfrom            = $userfrom;
+        $eventdata->userto              = $USER;
+        $eventdata->subject             = self::get_reply_subject($this->currentmessagedata->envelope->subject);
+        $eventdata->fullmessage         = $plainmessage;
+        $eventdata->fullmessageformat   = FORMAT_PLAIN;
+        $eventdata->fullmessagehtml     = $htmlmessage;
+        $eventdata->smallmessage        = $eventdata->fullmessage;
+        $eventdata->notification        = 1;
+
+        if (message_send($eventdata)) {
+            mtrace("---> Success notification sent to {$USER->email}.");
+        } else {
+            mtrace("---> Unable to send success notification.");
+        }
+        return true;
+    }
+
+    /**
+     * Return a formatted subject line for replies.
+     *
+     * @param $subject string The subject string
+     * @return string The formatted reply subject
+     */
+    private function get_reply_subject($subject) {
+        $prefix = get_string('replysubjectprefix', 'tool_messageinbound');
+        if (!(substr($subject, 0, strlen($prefix)) == $prefix)) {
+            $subject = $prefix . ' ' . $subject;
+        }
+
+        return $subject;
+    }
+}
diff --git a/admin/tool/messageinbound/classes/message/inbound/invalid_recipient_handler.php b/admin/tool/messageinbound/classes/message/inbound/invalid_recipient_handler.php
new file mode 100644 (file)
index 0000000..7ff3a69
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+/**
+ * A Handler to re-process messages which previously failed sender
+ * verification.
+ *
+ * @package    task_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\message\inbound;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/repository/lib.php');
+
+/**
+ * A Handler to re-process messages which previously failed sender
+ * verification.
+ *
+ * This may happen if the user did not use their registerd e-mail address,
+ * the verification hash used had expired, or if some erroneous content was
+ * introduced into the content hash.
+ *
+ * @package    task
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class invalid_recipient_handler extends \core\message\inbound\handler {
+
+    /**
+     * Do not allow changes to the address validation setting.
+     */
+    public function allow_validateaddress_change() {
+        return false;
+    }
+
+    /**
+     * Return a description for the current handler.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('invalid_recipient_handler', 'tool_messageinbound');
+    }
+
+    /**
+     * Return a name for the current handler.
+     * This appears in the admin pages as a human-readable name.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('invalid_recipient_handler_name', 'tool_messageinbound');
+    }
+
+    /**
+     * Process a message received and validated by the Inbound Message processor.
+     *
+     * @param $messagedata The Inbound Message record
+     * @param $messagedata The message data packet.
+     * @return bool Whether the message was successfully processed.
+     */
+    public function process_message(\stdClass $record, \stdClass $data) {
+        global $DB;
+
+        if (!$maildata = $DB->get_record('messageinbound_messagelist', array('id' => $record->datavalue))) {
+            // The message requested couldn't be found. Failing here will alert the user that we failed.
+            throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
+        }
+
+        mtrace("=== Request to re-process message {$record->datavalue} from server.");
+        mtrace("=== Message-Id:\t{$maildata->messageid}");
+        mtrace("=== Recipient:\t{$maildata->address}");
+
+        $manager = new \tool_messageinbound\manager();
+        return $manager->process_existing_message($maildata);
+    }
+
+}
diff --git a/admin/tool/messageinbound/classes/task/cleanup_task.php b/admin/tool/messageinbound/classes/task/cleanup_task.php
new file mode 100644 (file)
index 0000000..e507597
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * A scheduled task to handle cleanup of old, unconfirmed e-mails.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to handle cleanup of old, unconfirmed e-mails.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cleanup_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskcleanup', 'tool_messageinbound');
+    }
+
+    /**
+     * Execute the main Inbound Message pickup task.
+     */
+    public function execute() {
+        $manager = new \tool_messageinbound\manager();
+        return $manager->tidy_old_messages();
+    }
+}
diff --git a/admin/tool/messageinbound/classes/task/pickup_task.php b/admin/tool/messageinbound/classes/task/pickup_task.php
new file mode 100644 (file)
index 0000000..d10fb8c
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * A scheduled task to handle Inbound Message e-mail pickup.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to handle Inbound Message e-mail pickup.
+ *
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class pickup_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskpickup', 'tool_messageinbound');
+    }
+
+    /**
+     * Execute the main Inbound Message pickup task.
+     */
+    public function execute() {
+        $manager = new \tool_messageinbound\manager();
+        return $manager->pickup_messages();
+    }
+}
diff --git a/admin/tool/messageinbound/db/messageinbound_handlers.php b/admin/tool/messageinbound/db/messageinbound_handlers.php
new file mode 100644 (file)
index 0000000..50cb34a
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Handlers for tool_messageinbound.
+ *
+ * @package    task
+ * @category   tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$handlers = array(
+    array(
+        'classname'         => '\tool_messageinbound\message\inbound\invalid_recipient_handler',
+        'enabled'           => true,
+        'validateaddress'   => false,
+    ),
+);
diff --git a/admin/tool/messageinbound/db/messages.php b/admin/tool/messageinbound/db/messages.php
new file mode 100644 (file)
index 0000000..c425c48
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Message Providers for task_messageinbound.
+ *
+ * @package    task
+ * @category   messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$messageproviders = array (
+    // Invalid recipient handler.
+    'invalidrecipienthandler'   => array(),
+
+    // A generic message processing error.
+    'messageprocessingerror'    => array(),
+
+    // A generic message processing success message.
+    'messageprocessingsuccess'    => array(),
+);
diff --git a/admin/tool/messageinbound/db/tasks.php b/admin/tool/messageinbound/db/tasks.php
new file mode 100644 (file)
index 0000000..6e4c659
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * The Main Manager tasks.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => '\tool_messageinbound\task\pickup_task',
+        'blocking' => 0,
+        'minute' => '*',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+
+    array(
+        'classname' => '\tool_messageinbound\task\cleanup_task',
+        'blocking' => 0,
+        'minute' => '55',
+        'hour' => '1',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+);
diff --git a/admin/tool/messageinbound/index.php b/admin/tool/messageinbound/index.php
new file mode 100644 (file)
index 0000000..8aa16ad
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+/**
+ * Inbound Message Settings pages.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew NIcols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+admin_externalpage_setup('messageinbound_handlers');
+
+$classname = optional_param('classname', '', PARAM_RAW);
+
+$pageurl = new moodle_url('/admin/tool/messageinbound/index.php');
+
+if (empty($classname)) {
+    $renderer = $PAGE->get_renderer('tool_messageinbound');
+
+    $records = $DB->get_recordset('messageinbound_handlers', null, 'enabled desc', 'classname');
+    $instances = array();
+    foreach ($records as $record) {
+        $instances[] = \core\message\inbound\manager::get_handler($record->classname);
+    }
+
+    echo $OUTPUT->header();
+    echo $renderer->messageinbound_handlers_table($instances);
+    echo $OUTPUT->footer();
+
+} else {
+    // Retrieve the handler and its record.
+    $handler = \core\message\inbound\manager::get_handler($classname);
+    $record = \core\message\inbound\manager::record_from_handler($handler);
+
+    $formurl = new moodle_url($PAGE->url, array('classname' => $classname));
+    $mform = new tool_messageinbound_edit_handler_form($formurl, array(
+            'handler' => $handler,
+    ));
+
+    if ($mform->is_cancelled()) {
+        redirect($PAGE->url);
+    } else if ($data = $mform->get_data()) {
+        // Update the record from the form.
+        $record->defaultexpiration = (int) $data->defaultexpiration;
+
+        if ($handler->can_change_validateaddress()) {
+            $record->validateaddress = (int) $data->validateaddress;
+        }
+
+        if ($handler->can_change_enabled()) {
+            $record->enabled = (int) $data->enabled;
+        }
+        $DB->update_record('messageinbound_handlers', $record);
+        redirect($PAGE->url);
+    }
+
+    // Add the breadcrumb.
+    $pageurl->param('classname', $handler->classname);
+    $PAGE->navbar->add($handler->name, $pageurl);
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('editinghandler', 'tool_messageinbound', $handler->name));
+    $mform->set_data($record);
+    $mform->display();
+    echo $OUTPUT->footer();
+
+}
diff --git a/admin/tool/messageinbound/lang/en/tool_messageinbound.php b/admin/tool/messageinbound/lang/en/tool_messageinbound.php
new file mode 100644 (file)
index 0000000..19c5c75
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'tool_messageinbound', language 'en'
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['classname'] = 'Class name';
+$string['component'] = 'Component';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, you can use the [server]:[port], for example mail.example.com:587 format. If you leave this field blank, Moodle will use the default port for the type of mail server you specify.';
+$string['defaultexpiration'] = 'Default address expiry period';
+$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
+$string['description'] = 'Description';
+$string['domain'] = 'Email Domain';
+$string['edit'] = 'Edit';
+$string['edithandler'] = 'Edit settings for the {$a} handler';
+$string['editinghandler'] = 'Editing {$a}';
+$string['enabled'] = 'Enabled';
+$string['fixedvalidateaddress'] = 'Validate sender address';
+$string['fixedvalidateaddress_help'] = 'You cannot change the address validation for this handler. This may be because the handler requires a specific setting.';
+$string['fixedenabled_help'] = 'You cannot change the state of this handler. This may be because the handler is required by other handlers.';
+$string['incomingmailconfiguration'] = 'Incoming mail configuration';
+$string['incomingmailserversettings'] = 'Incoming mail server settings';
+$string['incomingmailserversettings_desc'] = 'Moodle is capable of connecting to appropriately configured IMAP servers. You can specify the settings used to connect to your IMAP server here.';
+$string['invalid_recipient_handler'] = 'If a valid message is received but the sender cannot be authenticated, the message is stored on the email server and the user is contacted using the email address in their user profile. The user is given the chance to reply to confirm the authenticity of the original message.
+
+This handler processes those replies.
+
+It is not possible to disable sender verification of this handler because the user may reply from an incorrect email address if their email client configuration is incorrect.';
+$string['invalid_recipient_handler_name'] = 'Invalid recipient handler';
+$string['invalidrecipientdescription'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
+$string['invalidrecipientdescriptionhtml'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
+$string['invalidrecipientfinal'] = 'The message you sent with subject "{$a->subject}" could not be authenticated. Please check that you are sending your message from the e-mail account listed in your Moodle profile.';
+$string['mailbox'] = 'Mailbox name';
+$string['mailboxconfiguration'] = 'Mailbox configuration';
+$string['mailboxdescription'] = '[mailbox]+subaddress@[domain]';
+$string['mailsettings'] = 'Mail settings';
+$string['message_handlers'] = 'Message handlers';
+$string['messageprocessingerror'] = 'You recently sent an e-mail to Moodle with the subject "{$a->subject}" but Moodle was unable to process it.
+
+The details of the error are shown below.
+
+{$a->error}';
+$string['messageprocessingerrorhtml'] = '<p>You recently sent an e-mail to Moodle with the subject "{$a->subject}" but Moodle was unable to process it.</p>
+<p>The details of the error are shown below.</p>
+<p>{$a->error}</p>';
+$string['messageprocessingfailed'] = 'Moodle was unable to process the e-mail you sent with subject "{$a->subject}". The following error was given: "{$a->message}".';
+$string['messageprocessingfailedunknown'] = 'Moodle was unable to process the e-mail you sent with subject "{$a->subject}". Contact your system administrator for further information.';
+$string['messageprocessingsuccess'] = '{$a->plain}
+
+If you do not wish to receive these notifications in the future, you can edit your personal messaging preferences by opening {$a->messagepreferencesurl} in your browser.';
+$string['messageprocessingsuccesshtml'] = '{$a->html}
+<p>If you do not wish to receive these notifications in the future, you can <a href="{$a->messagepreferencesurl}">edit your personal messaging preferences</a>.</p>';
+$string['messageinbound'] = 'Message Inbound';
+$string['messageinboundenabled'] = 'Enable incoming mail processing';
+$string['messageinboundenabled_desc'] = 'Incoming mail processing must be enabled in order for messages to be sent with the appropriate information.';
+$string['messageinboundgeneralconfiguration'] = 'General configuration';
+$string['messageinboundgeneralconfiguration_desc'] = 'Inbound message processing allows you to receive and process email within Moodle. This has applications such as sending email replies to forum posts or adding files to a user\'s private files.';
+$string['messageinboundhost'] = 'Incoming Mail Server';
+$string['messageinboundhostpass'] = 'Password';
+$string['messageinboundhostpass_desc'] = 'This is the password your service provider will have provided to log into your e-mail account with.';
+$string['messageinboundhostssl'] = 'Use SSL';
+$string['messageinboundhostssl_desc'] = 'Some mail servers support an additional level of security by encrypting communication between Moodle and your server. We recommend using this SSL encryption if your server supports it.';
+$string['messageinboundhosttype'] = 'Server type';
+$string['messageinboundhostuser'] = 'Username';
+$string['messageinboundhostuser_desc'] = 'This is the username your service provider will have provided to log into your e-mail account with.';
+$string['messageinboundmailboxconfiguration_desc'] = 'When messages are sent out, they fit into the format address+data@example.com. To reliably generate addresses from Moodle, please specify the address that you would normally use before the @ sign, and the domain after the @ sign separately. For example, the Mailbox name in the example would be "address", and the E-mail domain would be "example.com". You should use a dedicated e-mail account for this purpose.';
+$string['messageprovider:invalidrecipienthandler'] = 'Messages to confirm that an inbound messages came from you';
+$string['messageprovider:messageprocessingerror'] = 'Warning when an inbound message could not be processed';
+$string['messageprovider:messageprocessingsuccess'] = 'Confirmation that a message was successfully processed';
+$string['noencryption'] = 'Off - No encryption';
+$string['noexpiry'] = 'No expiry';
+$string['oldmessagenotfound'] = 'You tried to manually authenticate a message, but the message could not be found. This could be because it has already been processed, or because the message expired.';
+$string['oneday'] = 'One day';
+$string['onehour'] = 'One hour';
+$string['oneweek'] = 'One week';
+$string['oneyear'] = 'One year';
+$string['pluginname'] = 'Inbound message configuration';
+$string['replysubjectprefix'] = 'Re:';
+$string['requirevalidation'] = 'Validate sender address';
+$string['name'] = 'Name';
+$string['ssl'] = 'SSL (Auto-detect SSL version)';
+$string['sslv2'] = 'SSLv2 (Force SSL Version 2)';
+$string['sslv3'] = 'SSLv2 (Force SSL Version 3)';
+$string['taskcleanup'] = 'Cleanup of unverified incoming email';
+$string['taskpickup'] = 'Incoming email pickup';
+$string['tls'] = 'TLS (TLS; started via protocol-level negotiation over unencrypted channel; RECOMMENDED way of initiating secure connection)';
+$string['tlsv1'] = 'TLSv1 (TLS direct version 1.x connection to server)';
+$string['validateaddress'] = 'Validate sender email address';
+$string['validateaddress_help'] = 'When a message is received from a user, Moodle attempts to validate the message by comparing the email address of the sender with the email address in their user profile.
+
+If the sender does not match, then the user is sent a notification to confirm that they really did send the email.
+
+If this setting is disabled, then the email address of the sender is not checked at all.';
diff --git a/admin/tool/messageinbound/renderer.php b/admin/tool/messageinbound/renderer.php
new file mode 100644 (file)
index 0000000..d383d74
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * Output rendering for the plugin.
+ *
+ * @package     tool_messageinbound
+ * @copyright   2014 Andrew Nicols
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implements the plugin renderer
+ *
+ * @copyright 2014 Andrew Nicols
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_messageinbound_renderer extends plugin_renderer_base {
+
+    /**
+     * Render a table listing all of the Inbound Message handlers.
+     *
+     * @param array $handlers - list of all messageinbound handlers.
+     * @return string HTML to output.
+     */
+    public function messageinbound_handlers_table(array $handlers) {
+        global $CFG;
+
+        $table = new html_table();
+        $handlername = new html_table_cell(get_string('name', 'tool_messageinbound') . "\n" .
+                html_writer::tag('span', get_string('classname', 'tool_messageinbound'), array('class' => 'handler-function')));
+
+        // Prepare some of the rows with additional styling.
+        $enabled = new html_table_cell(get_string('enabled', 'tool_messageinbound'));
+        $enabled->attributes['class'] = 'state';
+        $edit = new html_table_cell(get_string('edit', 'tool_messageinbound'));
+        $edit->attributes['class'] = 'edit';
+        $table->head  = array(
+                $handlername,
+                get_string('description', 'tool_messageinbound'),
+                $enabled,
+                $edit,
+            );
+        $table->attributes['class'] = 'admintable generaltable messageinboundhandlers';
+
+        $yes = get_string('yes');
+        $no = get_string('no');
+
+        $data = array();
+
+        // Options for description formatting.
+        $descriptionoptions = new stdClass();
+        $descriptionoptions->trusted = false;
+        $descriptionoptions->noclean = false;
+        $descriptionoptions->smiley = false;
+        $descriptionoptions->filter = false;
+        $descriptionoptions->para = true;
+        $descriptionoptions->newlines = false;
+        $descriptionoptions->overflowdiv = true;
+
+        $editurlbase = new moodle_url('/admin/tool/messageinbound/index.php');
+        foreach ($handlers as $handler) {
+            $handlername = new html_table_cell($handler->name . "\n" .
+                    html_writer::tag('span', $handler->classname, array('class' => 'handler-function')));
+            $handlername->header = true;
+
+            $editurl = new moodle_url($editurlbase, array('classname' => $handler->classname));
+            $editlink = $this->action_icon($editurl, new pix_icon('t/edit',
+                    get_string('edithandler', 'tool_messageinbound', $handler->classname)));
+
+            // Prepare some of the rows with additional styling.
+            $enabled = new html_table_cell($handler->enabled ? $yes : $no);
+            $enabled->attributes['class'] = 'state';
+            $edit = new html_table_cell($editlink);
+            $edit->attributes['class'] = 'edit';
+
+            // Add the row.
+            $row = new html_table_row(array(
+                        $handlername,
+                        format_text($handler->description, FORMAT_MARKDOWN, $descriptionoptions),
+                        $enabled,
+                        $edit,
+                    ));
+
+            if (!$handler->enabled) {
+                $row->attributes['class'] = 'disabled';
+            }
+            $data[] = $row;
+        }
+        $table->data = $data;
+        return html_writer::table($table);
+    }
+
+}
diff --git a/admin/tool/messageinbound/settings.php b/admin/tool/messageinbound/settings.php
new file mode 100644 (file)
index 0000000..72d23ab
--- /dev/null
@@ -0,0 +1,87 @@
+<?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/>.
+
+/**
+ * Inbound Message Settings.
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+    $category = new admin_category('messageinbound', new lang_string('incomingmailconfiguration', 'tool_messageinbound'));
+
+    // Create a settings page for all of the mail server settings.
+    $settings = new admin_settingpage('messageinbound_mailsettings', new lang_string('mailsettings', 'tool_messageinbound'));
+
+    $settings->add(new admin_setting_heading('messageinbound_generalconfiguration',
+            new lang_string('messageinboundgeneralconfiguration', 'tool_messageinbound'),
+            new lang_string('messageinboundgeneralconfiguration_desc', 'tool_messageinbound'), ''));
+    $settings->add(new admin_setting_configcheckbox('messageinbound_enabled',
+            new lang_string('messageinboundenabled', 'tool_messageinbound'),
+            new lang_string('messageinboundenabled_desc', 'tool_messageinbound'), 0));
+
+    // These settings are used when generating a Inbound Message address.
+    $settings->add(new admin_setting_heading('messageinbound_mailboxconfiguration',
+            new lang_string('mailboxconfiguration', 'tool_messageinbound'),
+            new lang_string('messageinboundmailboxconfiguration_desc', 'tool_messageinbound'), ''));
+    $settings->add(new admin_setting_configtext('messageinbound_mailbox',
+            new lang_string('mailbox', 'tool_messageinbound'),
+            null, '', PARAM_RAW));
+    $settings->add(new admin_setting_configtext('messageinbound_domain',
+            new lang_string('domain', 'tool_messageinbound'),
+            null, '', PARAM_RAW));
+
+    // These settings are used when checking the incoming mailbox for mail.
+    $settings->add(new admin_setting_heading('messageinbound_serversettings',
+            new lang_string('incomingmailserversettings', 'tool_messageinbound'),
+            new lang_string('incomingmailserversettings_desc', 'tool_messageinbound'), ''));
+    $settings->add(new admin_setting_configtext('messageinbound_host',
+            new lang_string('messageinboundhost', 'tool_messageinbound'),
+            new lang_string('configmessageinboundhost', 'tool_messageinbound'), '', PARAM_RAW));
+
+    $options = array(
+        ''          => get_string('noencryption',   'tool_messageinbound'),
+        'ssl'       => get_string('ssl',            'tool_messageinbound'),
+        'sslv2'     => get_string('sslv2',          'tool_messageinbound'),
+        'sslv3'     => get_string('sslv3',          'tool_messageinbound'),
+        'tls'       => get_string('tls',            'tool_messageinbound'),
+        'tlsv1'     => get_string('tlsv1',          'tool_messageinbound'),
+    );
+    $settings->add(new admin_setting_configselect('messageinbound_hostssl',
+            new lang_string('messageinboundhostssl', 'tool_messageinbound'),
+            new lang_string('messageinboundhostssl_desc', 'tool_messageinbound'), 'ssl', $options));
+
+    $settings->add(new admin_setting_configtext('messageinbound_hostuser',
+            new lang_string('messageinboundhostuser', 'tool_messageinbound'),
+            new lang_string('messageinboundhostuser_desc', 'tool_messageinbound'), '', PARAM_NOTAGS));
+    $settings->add(new admin_setting_configpasswordunmask('messageinbound_hostpass',
+            new lang_string('messageinboundhostpass', 'tool_messageinbound'),
+            new lang_string('messageinboundhostpass_desc', 'tool_messageinbound'), ''));
+
+    $category->add('messageinbound', $settings);
+
+    // Link to the external page for Inbound Message handler configuration.
+    $category->add('messageinbound', new admin_externalpage('messageinbound_handlers',
+            new lang_string('message_handlers', 'tool_messageinbound'),
+            "$CFG->wwwroot/$CFG->admin/tool/messageinbound/index.php"));
+
+    // Add the category to the admin tree.
+    $ADMIN->add('server', $category);
+}
diff --git a/admin/tool/messageinbound/styles.css b/admin/tool/messageinbound/styles.css
new file mode 100644 (file)
index 0000000..fba5ddc
--- /dev/null
@@ -0,0 +1,11 @@
+#page-admin-tool-messageinbound-index .handler-function {
+    display: block;
+    padding: 0 0.5em;
+    color: #888;
+    font-size: 0.75em;
+}
+
+#page-admin-tool-messageinbound-index .state,
+#page-admin-tool-messageinbound-index .edit {
+    text-align: center;
+}
diff --git a/admin/tool/messageinbound/version.php b/admin/tool/messageinbound/version.php
new file mode 100644 (file)
index 0000000..57e55ae
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin version info
+ *
+ * @package    tool_messageinbound
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2014091200;
+$plugin->requires  = 2014050800;
+$plugin->component = 'tool_messageinbound';
index e79c667..4dd37be 100644 (file)
@@ -940,8 +940,8 @@ class backup_gradebook_structure_step extends backup_structure_step {
             'iteminstance', 'itemnumber', 'iteminfo', 'idnumber',
             'calculation', 'gradetype', 'grademax', 'grademin',
             'scaleid', 'outcomeid', 'gradepass', 'multfactor',
-            'plusfactor', 'aggregationcoef', 'sortorder', 'display',
-            'decimals', 'hidden', 'locked', 'locktime',
+            'plusfactor', 'aggregationcoef', 'aggregationcoef2', 'weightoverride',
+            'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
             'needsupdate', 'timecreated', 'timemodified'));
 
         $grade_grades = new backup_nested_element('grade_grades');
@@ -950,7 +950,8 @@ class backup_gradebook_structure_step extends backup_structure_step {
             'rawscaleid', 'usermodified', 'finalgrade', 'hidden',
             'locked', 'locktime', 'exported', 'overridden',
             'excluded', 'feedback', 'feedbackformat', 'information',
-            'informationformat', 'timecreated', 'timemodified'));
+            'informationformat', 'timecreated', 'timemodified',
+            'aggregationstatus', 'aggregationweight'));
 
         //grade_categories
         $grade_categories = new backup_nested_element('grade_categories');
@@ -2163,8 +2164,8 @@ class backup_activity_grades_structure_step extends backup_structure_step {
             'iteminstance', 'itemnumber', 'iteminfo', 'idnumber',
             'calculation', 'gradetype', 'grademax', 'grademin',
             'scaleid', 'outcomeid', 'gradepass', 'multfactor',
-            'plusfactor', 'aggregationcoef', 'sortorder', 'display',
-            'decimals', 'hidden', 'locked', 'locktime',
+            'plusfactor', 'aggregationcoef', 'aggregationcoef2', 'weightoverride',
+            'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
             'needsupdate', 'timecreated', 'timemodified'));
 
         $grades = new backup_nested_element('grade_grades');
@@ -2174,7 +2175,8 @@ class backup_activity_grades_structure_step extends backup_structure_step {
             'rawscaleid', 'usermodified', 'finalgrade', 'hidden',
             'locked', 'locktime', 'exported', 'overridden',
             'excluded', 'feedback', 'feedbackformat', 'information',
-            'informationformat', 'timecreated', 'timemodified'));
+            'informationformat', 'timecreated', 'timemodified',
+            'aggregationstatus', 'aggregationweight'));
 
         $letters = new backup_nested_element('grade_letters');
 
index 8873117..4715517 100644 (file)
@@ -62,7 +62,7 @@ if ($context->contextlevel == CONTEXT_COURSECAT) {
 }
 $PAGE->navbar->add(get_string('assign', 'cohort'));
 
-$PAGE->set_title(get_string('cohort:assign', 'cohort'));
+$PAGE->set_title(get_string('assigncohorts', 'cohort'));
 $PAGE->set_heading($COURSE->fullname);
 
 echo $OUTPUT->header();
index dbee96f..b8c73ba 100644 (file)
@@ -30,6 +30,8 @@ require($CFG->dirroot.'/cohort/edit_form.php');
 $id        = optional_param('id', 0, PARAM_INT);
 $contextid = optional_param('contextid', 0, PARAM_INT);
 $delete    = optional_param('delete', 0, PARAM_BOOL);
+$show      = optional_param('show', 0, PARAM_BOOL);
+$hide      = optional_param('hide', 0, PARAM_BOOL);
 $confirm   = optional_param('confirm', 0, PARAM_BOOL);
 $returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
 
@@ -98,6 +100,22 @@ if ($delete and $cohort->id) {
     die;
 }
 
+if ($show && $cohort->id && confirm_sesskey()) {
+    if (!$cohort->visible) {
+        $record = (object)array('id' => $cohort->id, 'visible' => 1, 'contextid' => $cohort->contextid);
+        cohort_update_cohort($record);
+    }
+    redirect($returnurl);
+}
+
+if ($hide && $cohort->id && confirm_sesskey()) {
+    if ($cohort->visible) {
+        $record = (object)array('id' => $cohort->id, 'visible' => 0, 'contextid' => $cohort->contextid);
+        cohort_update_cohort($record);
+    }
+    redirect($returnurl);
+}
+
 $editoroptions = array('maxfiles'=>0, 'context'=>$context);
 if ($cohort->id) {
     // Edit existing.
index 72a5db7..d4abb88 100644 (file)
@@ -47,6 +47,10 @@ class cohort_edit_form extends moodleform {
         $mform->addElement('text', 'idnumber', get_string('idnumber', 'cohort'), 'maxlength="254" size="50"');
         $mform->setType('idnumber', PARAM_RAW); // Idnumbers are plain text, must not be changed.
 
+        $mform->addElement('advcheckbox', 'visible', get_string('visible', 'cohort'));
+        $mform->setDefault('visible', 1);
+        $mform->addHelpButton('visible', 'visible', 'cohort');
+
         $mform->addElement('editor', 'description_editor', get_string('description', 'cohort'), null, $editoroptions);
         $mform->setType('description_editor', PARAM_RAW);
 
index 2def361..88defef 100644 (file)
@@ -51,6 +51,7 @@ class core_cohort_external extends external_api {
                             'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
                             'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
+                            'visible' => new external_value(PARAM_BOOL, 'cohort visible', VALUE_OPTIONAL, true),
                         )
                     )
                 )
@@ -133,6 +134,7 @@ class core_cohort_external extends external_api {
                     'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
+                    'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
                 )
             )
         );
@@ -263,6 +265,7 @@ class core_cohort_external extends external_api {
                     'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
+                    'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
                 )
             )
         );
@@ -293,6 +296,7 @@ class core_cohort_external extends external_api {
                             'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
                             'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
+                            'visible' => new external_value(PARAM_BOOL, 'cohort visible', VALUE_OPTIONAL),
                         )
                     )
                 )
index f139e23..89a8b92 100644 (file)
@@ -147,20 +147,36 @@ foreach($cohorts['cohorts'] as $cohort) {
         $cohortcanassign = has_capability('moodle/cohort:assign', $cohortcontext);
 
         $urlparams = array('id' => $cohort->id, 'returnurl' => $baseurl->out_as_local_url());
+        $showhideurl = new moodle_url('/cohort/edit.php', $urlparams + array('sesskey' => sesskey()));
+        if ($cohort->visible) {
+            $showhideurl->param('hide', 1);
+            $visibleimg = html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/hide'), 'alt' => get_string('hide'), 'class' => 'iconsmall'));
+            $buttons[] = html_writer::link($showhideurl, $visibleimg, array('title' => get_string('hide')));
+        } else {
+            $showhideurl->param('show', 1);
+            $visibleimg = html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/show'), 'alt' => get_string('show'), 'class' => 'iconsmall'));
+            $buttons[] = html_writer::link($showhideurl, $visibleimg, array('title' => get_string('show')));
+        }
         if ($cohortmanager) {
             $buttons[] = html_writer::link(new moodle_url('/cohort/edit.php', $urlparams + array('delete' => 1)),
-                html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/delete'), 'alt' => get_string('delete'), 'class' => 'iconsmall')));
+                html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/delete'), 'alt' => get_string('delete'), 'class' => 'iconsmall')),
+                array('title' => get_string('delete')));
             $buttons[] = html_writer::link(new moodle_url('/cohort/edit.php', $urlparams),
-                html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/edit'), 'alt' => get_string('edit'), 'class' => 'iconsmall')));
+                html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/edit'), 'alt' => get_string('edit'), 'class' => 'iconsmall')),
+                array('title' => get_string('edit')));
         }
         if ($cohortcanassign) {
             $buttons[] = html_writer::link(new moodle_url('/cohort/assign.php', $urlparams),
-                html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('i/users'), 'alt' => get_string('assign', 'core_cohort'), 'class' => 'iconsmall')));
+                html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('i/users'), 'alt' => get_string('assign', 'core_cohort'), 'class' => 'iconsmall')),
+                array('title' => get_string('assign', 'core_cohort')));
         }
     }
     $line[] = implode(' ', $buttons);
 
-    $data[] = $line;
+    $data[] = $row = new html_table_row($line);
+    if (!$cohort->visible) {
+        $row->attributes['class'] = 'dimmed_text';
+    }
 }
 $table = new html_table();
 $table->head  = array(get_string('name', 'cohort'), get_string('idnumber', 'cohort'), get_string('description', 'cohort'),
index 74d1553..e859cac 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+define('COHORT_ALL', 0);
+define('COHORT_COUNT_MEMBERS', 1);
+define('COHORT_COUNT_ENROLLED_MEMBERS', 3);
+define('COHORT_WITH_MEMBERS_ONLY', 5);
+define('COHORT_WITH_ENROLLED_MEMBERS_ONLY', 17);
+define('COHORT_WITH_NOTENROLLED_MEMBERS_ONLY', 23);
+
 /**
  * Add new cohort.
  *
@@ -45,6 +52,9 @@ function cohort_add_cohort($cohort) {
     if (!isset($cohort->descriptionformat)) {
         $cohort->descriptionformat = FORMAT_HTML;
     }
+    if (!isset($cohort->visible)) {
+        $cohort->visible = 1;
+    }
     if (empty($cohort->component)) {
         $cohort->component = '';
     }
@@ -200,76 +210,143 @@ function cohort_is_member($cohortid, $userid) {
 }
 
 /**
- * Returns list of cohorts from course parent contexts.
+ * Returns the list of cohorts visible to the current user in the given course.
  *
- * Note: this function does not implement any capability checks,
- *       it means it may disclose existence of cohorts,
- *       make sure it is displayed to users with appropriate rights only.
+ * The following fields are returned in each record: id, name, contextid, idnumber, visible
+ * Fields memberscnt and enrolledcnt will be also returned if requested
  *
- * @param  stdClass $course
- * @param  bool $onlyenrolled true means include only cohorts with enrolled users
- * @return array of cohort names with number of enrolled users
+ * @param context $currentcontext
+ * @param int $withmembers one of the COHORT_XXX constants that allows to return non empty cohorts only
+ *      or cohorts with enroled/not enroled users, or just return members count
+ * @param int $offset
+ * @param int $limit
+ * @param string $search
+ * @return array
  */
-function cohort_get_visible_list($course, $onlyenrolled=true) {
+function cohort_get_available_cohorts($currentcontext, $withmembers = 0, $offset = 0, $limit = 25, $search = '') {
     global $DB;
 
-    $context = context_course::instance($course->id);
-    list($esql, $params) = get_enrolled_sql($context);
-    list($parentsql, $params2) = $DB->get_in_or_equal($context->get_parent_context_ids(), SQL_PARAMS_NAMED);
-    $params = array_merge($params, $params2);
+    $params = array();
 
-    if ($onlyenrolled) {
-        $left = "";
-        $having = "HAVING COUNT(u.id) > 0";
-    } else {
-        $left = "LEFT";
-        $having = "";
+    // Build context subquery. Find the list of parent context where user is able to see any or visible-only cohorts.
+    // Since this method is normally called for the current course all parent contexts are already preloaded.
+    $contextsany = array_filter($currentcontext->get_parent_context_ids(),
+        create_function('$a', 'return has_capability("moodle/cohort:view", context::instance_by_id($a));'));
+    $contextsvisible = array_diff($currentcontext->get_parent_context_ids(), $contextsany);
+    if (empty($contextsany) && empty($contextsvisible)) {
+        // User does not have any permissions to view cohorts.
+        return array();
+    }
+    $subqueries = array();
+    if (!empty($contextsany)) {
+        list($parentsql, $params1) = $DB->get_in_or_equal($contextsany, SQL_PARAMS_NAMED, 'ctxa');
+        $subqueries[] = 'c.contextid ' . $parentsql;
+        $params = array_merge($params, $params1);
+    }
+    if (!empty($contextsvisible)) {
+        list($parentsql, $params1) = $DB->get_in_or_equal($contextsvisible, SQL_PARAMS_NAMED, 'ctxv');
+        $subqueries[] = '(c.visible = 1 AND c.contextid ' . $parentsql. ')';
+        $params = array_merge($params, $params1);
+    }
+    $wheresql = '(' . implode(' OR ', $subqueries) . ')';
+
+    // Build the rest of the query.
+    $fromsql = "";
+    $fieldssql = 'c.id, c.name, c.contextid, c.idnumber, c.visible';
+    $groupbysql = '';
+    $havingsql = '';
+    if ($withmembers) {
+        $groupbysql = " GROUP BY $fieldssql";
+        $fromsql = " LEFT JOIN {cohort_members} cm ON cm.cohortid = c.id ";
+        $fieldssql .= ', COUNT(DISTINCT cm.userid) AS memberscnt';
+        if (in_array($withmembers,
+                array(COHORT_COUNT_ENROLLED_MEMBERS, COHORT_WITH_ENROLLED_MEMBERS_ONLY, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY))) {
+            list($esql, $params2) = get_enrolled_sql($currentcontext);
+            $fromsql .= " LEFT JOIN ($esql) u ON u.id = cm.userid ";
+            $params = array_merge($params2, $params);
+            $fieldssql .= ', COUNT(DISTINCT u.id) AS enrolledcnt';
+        }
+        if ($withmembers == COHORT_WITH_MEMBERS_ONLY) {
+            $havingsql = " HAVING COUNT(DISTINCT cm.userid) > 0";
+        } else if ($withmembers == COHORT_WITH_ENROLLED_MEMBERS_ONLY) {
+            $havingsql = " HAVING COUNT(DISTINCT u.id) > 0";
+        } else if ($withmembers == COHORT_WITH_NOTENROLLED_MEMBERS_ONLY) {
+            $havingsql = " HAVING COUNT(DISTINCT cm.userid) > COUNT(DISTINCT u.id)";
+        }
+    }
+    if ($search) {
+        list($searchsql, $searchparams) = cohort_get_search_query($search);
+        $wheresql .= ' AND ' . $searchsql;
+        $params = array_merge($params, $searchparams);
     }
 
-    $sql = "SELECT c.id, c.name, c.contextid, c.idnumber, COUNT(u.id) AS cnt
+    $sql = "SELECT $fieldssql
               FROM {cohort} c
-        $left JOIN ({cohort_members} cm
-                   JOIN ($esql) u ON u.id = cm.userid) ON cm.cohortid = c.id
-             WHERE c.contextid $parentsql
-          GROUP BY c.id, c.name, c.contextid, c.idnumber
-           $having
+              $fromsql
+             WHERE $wheresql
+             $groupbysql
+             $havingsql
           ORDER BY c.name, c.idnumber";
 
-    $cohorts = $DB->get_records_sql($sql, $params);
+    return $DB->get_records_sql($sql, $params, $offset, $limit);
+}
+
+/**
+ * Check if cohort exists and user is allowed to access it from the given context.
+ *
+ * @param stdClass|int $cohortorid cohort object or id
+ * @param context $currentcontext current context (course) where visibility is checked
+ * @return boolean
+ */
+function cohort_can_view_cohort($cohortorid, $currentcontext) {
+    global $DB;
+    if (is_numeric($cohortorid)) {
+        $cohort = $DB->get_record('cohort', array('id' => $cohortorid), 'id, contextid, visible');
+    } else {
+        $cohort = $cohortorid;
+    }
 
-    foreach ($cohorts as $cid=>$cohort) {
-        $cohorts[$cid] = format_string($cohort->name, true, array('context'=>$cohort->contextid));
-        if ($cohort->cnt) {
-            $cohorts[$cid] .= ' (' . $cohort->cnt . ')';
+    if ($cohort && in_array($cohort->contextid, $currentcontext->get_parent_context_ids())) {
+        if ($cohort->visible) {
+            return true;
+        }
+        $cohortcontext = context::instance_by_id($cohort->contextid);
+        if (has_capability('moodle/cohort:view', $cohortcontext)) {
+            return true;
         }
     }
-
-    return $cohorts;
+    return false;
 }
 
 /**
  * Produces a part of SQL query to filter cohorts by the search string
  *
- * Called from {@link cohort_get_cohorts()} and {@link cohort_get_all_cohorts()}
+ * Called from {@link cohort_get_cohorts()}, {@link cohort_get_all_cohorts()} and {@link cohort_get_available_cohorts()}
  *
  * @access private
  *
- * @param string $search
- * @return array of two elements - SQL condition and array of unnamed parameters
+ * @param string $search search string
+ * @param string $tablealias alias of cohort table in the SQL query (highly recommended if other tables are used in query)
+ * @return array of two elements - SQL condition and array of named parameters
  */
-function cohort_get_search_query($search) {
+function cohort_get_search_query($search, $tablealias = '') {
     global $DB;
     $params = array();
     if (empty($search)) {
         // This function should not be called if there is no search string, just in case return dummy query.
         return array('1=1', $params);
     }
+    if ($tablealias && substr($tablealias, -1) !== '.') {
+        $tablealias .= '.';
+    }
     $searchparam = '%' . $DB->sql_like_escape($search) . '%';
     $conditions = array();
     $fields = array('name', 'idnumber', 'description');
+    $cnt = 0;
     foreach ($fields as $field) {
-        $conditions[] = $DB->sql_like($field, "?", false);
-        $params[] = $searchparam;
+        $conditions[] = $DB->sql_like($tablealias . $field, ':csearch' . $cnt, false);
+        $params['csearch' . $cnt] = $searchparam;
+        $cnt++;
     }
     $sql = '(' . implode(' OR ', $conditions) . ')';
     return array($sql, $params);
@@ -293,8 +370,8 @@ function cohort_get_cohorts($contextid, $page = 0, $perpage = 25, $search = '')
     $fields = "SELECT *";
     $countfields = "SELECT COUNT(1)";
     $sql = " FROM {cohort}
-             WHERE contextid = ?";
-    $params = array($contextid);
+             WHERE contextid = :contextid";
+    $params = array('contextid' => $contextid);
     $order = " ORDER BY name ASC, idnumber ASC";
 
     if (!empty($search)) {
@@ -335,7 +412,7 @@ function cohort_get_all_cohorts($page = 0, $perpage = 25, $search = '') {
     $wheresql = '';
 
     if ($excludedcontexts = cohort_get_invisible_contexts()) {
-        list($excludedsql, $excludedparams) = $DB->get_in_or_equal($excludedcontexts, SQL_PARAMS_QM, null, false);
+        list($excludedsql, $excludedparams) = $DB->get_in_or_equal($excludedcontexts, SQL_PARAMS_NAMED, 'excl', false);
         $wheresql = ' WHERE c.contextid '.$excludedsql;
         $params = array_merge($params, $excludedparams);
     }
@@ -343,7 +420,7 @@ function cohort_get_all_cohorts($page = 0, $perpage = 25, $search = '') {
     $totalcohorts = $allcohorts = $DB->count_records_sql($countfields . $sql . $wheresql, $params);
 
     if (!empty($search)) {
-        list($searchcondition, $searchparams) = cohort_get_search_query($search);
+        list($searchcondition, $searchparams) = cohort_get_search_query($search, 'c');
         $wheresql .= ($wheresql ? ' AND ' : ' WHERE ') . $searchcondition;
         $params = array_merge($params, $searchparams);
         $totalcohorts = $DB->count_records_sql($countfields . $sql . $wheresql, $params);
@@ -429,4 +506,4 @@ function cohort_edit_controls(context $context, moodle_url $currenturl) {
         return new tabtree($tabs, $currenttab);
     }
     return null;
-}
\ No newline at end of file
+}
diff --git a/cohort/tests/behat/access_visible_cohorts.feature b/cohort/tests/behat/access_visible_cohorts.feature
new file mode 100644 (file)
index 0000000..03722ee
--- /dev/null
@@ -0,0 +1,127 @@
+@core @core_cohort @enrol_cohort
+Feature: Access visible and hidden cohorts
+  In order to enrol users from cohorts
+  As an manager or teacher
+  I need to be able to view the list of cohorts defined above the course
+
+  Background:
+    Given the following "categories" exist:
+      | name  | category | idnumber |
+      | Cat 1 | 0        | CAT1     |
+      | Cat 2 | 0        | CAT2     |
+    And the following "cohorts" exist:
+      | name                 | idnumber | visible |
+      | System cohort        | CV0      | 1       |
+      | System hidden cohort | CH0      | 0       |
+      | System empty cohort  | CVE0     | 1       |
+    And the following "cohorts" exist:
+      | name                        | idnumber | contextlevel | reference | visible |
+      | Cohort in category 1        | CV1      | Category     | CAT1      | 1       |
+      | Cohort in category 2        | CV2      | Category     | CAT2      | 1       |
+      | Cohort hidden in category 1 | CH1      | Category     | CAT1      | 0       |
+      | Cohort empty in category 1  | CVE1     | Category     | CAT1      | 1       |
+    Given the following "users" exist:
+      | username | firstname | lastname | email            |
+      | user1    | First     | User     | first@user.com   |
+      | user2    | Second    | User     | second@user.com  |
+      | student  | Sam       | User     | student@user.com |
+      | teacher  | Terry     | User     | teacher@user.com |
+    And the following "cohort members" exist:
+      | user    | cohort |
+      | student | CV0   |
+      | student | CV1   |
+      | student | CV2   |
+      | student | CH0   |
+      | student | CH1   |
+    And the following "role assigns" exist:
+      | user  | role    | contextlevel | reference |
+      | user1 | manager | System       |           |
+      | user2 | manager | Category     | CAT1      |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | CAT1     |
+    And the following "course enrolments" exist:
+      | user    | course | role           |
+      | teacher | C1     | editingteacher |
+
+  @javascript
+  Scenario: Teacher can see visible cohorts defined in the above contexts
+    When I log in as "teacher"
+    And I follow "Course 1"
+    And I navigate to "Enrolment methods" node in "Course administration > Users"
+    And I set the field "Add method" to "Cohort sync"
+    Then the "Cohort" select box should contain "Cohort in category 1"
+    And the "Cohort" select box should contain "System cohort"
+    And the "Cohort" select box should not contain "Cohort hidden in category 1"
+    And the "Cohort" select box should not contain "System hidden cohort"
+    And the "Cohort" select box should not contain "Cohort in category 2"
+    And the "Cohort" select box should contain "Cohort empty in category 1"
+    And the "Cohort" select box should contain "System empty cohort"
+    And I set the field "Cohort" to "System cohort"
+    And I press "Add method"
+    And I navigate to "Enrolled users" node in "Course administration > Users"
+    And I should see "student@user.com"
+    And I navigate to "Groups" node in "Course administration > Users"
+    And I press "Auto-create groups"
+    And the "Select members from cohort" select box should contain "Cohort in category 1"
+    And the "Select members from cohort" select box should contain "System cohort"
+    And the "Select members from cohort" select box should not contain "Cohort hidden in category 1"
+    And the "Select members from cohort" select box should not contain "System hidden cohort"
+    And the "Select members from cohort" select box should not contain "Cohort in category 2"
+    And the "Select members from cohort" select box should not contain "Cohort empty in category 1"
+    And the "Select members from cohort" select box should not contain "System empty cohort"
+
+  @javascript
+  Scenario: System manager can see all cohorts defined in the above contexts
+    When I log in as "user1"
+    And I follow "Course 1"
+    And I navigate to "Enrolment methods" node in "Course administration > Users"
+    And I set the field "Add method" to "Cohort sync"
+    Then the "Cohort" select box should contain "Cohort in category 1"
+    And the "Cohort" select box should contain "System cohort"
+    And the "Cohort" select box should contain "Cohort hidden in category 1"
+    And the "Cohort" select box should contain "System hidden cohort"
+    And the "Cohort" select box should not contain "Cohort in category 2"
+    And the "Cohort" select box should contain "Cohort empty in category 1"
+    And the "Cohort" select box should contain "System empty cohort"
+    And I set the field "Cohort" to "System cohort"
+    And I press "Add method"
+    And I navigate to "Enrolled users" node in "Course administration > Users"
+    And I should see "student@user.com"
+    And I navigate to "Groups" node in "Course administration > Users"
+    And I press "Auto-create groups"
+    And the "Select members from cohort" select box should contain "Cohort in category 1"
+    And the "Select members from cohort" select box should contain "System cohort"
+    And the "Select members from cohort" select box should contain "Cohort hidden in category 1"
+    And the "Select members from cohort" select box should contain "System hidden cohort"
+    And the "Select members from cohort" select box should not contain "Cohort in category 2"
+    And the "Select members from cohort" select box should not contain "Cohort empty in category 1"
+    And the "Select members from cohort" select box should not contain "System empty cohort"
+
+  @javascript
+  Scenario: Category manager can see all cohorts defined in his category and visible cohorts defined above
+    When I log in as "user2"
+    And I follow "Course 1"
+    And I navigate to "Enrolment methods" node in "Course administration > Users"
+    And I set the field "Add method" to "Cohort sync"
+    Then the "Cohort" select box should contain "Cohort in category 1"
+    And the "Cohort" select box should contain "System cohort"
+    And the "Cohort" select box should contain "Cohort hidden in category 1"
+    And the "Cohort" select box should not contain "System hidden cohort"
+    And the "Cohort" select box should not contain "Cohort in category 2"
+    And the "Cohort" select box should contain "Cohort empty in category 1"
+    And the "Cohort" select box should contain "System empty cohort"
+    And I set the field "Cohort" to "System cohort"
+    And I press "Add method"
+    And I navigate to "Enrolled users" node in "Course administration > Users"
+    And I should see "student@user.com"
+    And I navigate to "Groups" node in "Course administration > Users"
+    And I press "Auto-create groups"
+    And the "Select members from cohort" select box should contain "Cohort in category 1"
+    And the "Select members from cohort" select box should contain "System cohort"
+    And the "Select members from cohort" select box should contain "Cohort hidden in category 1"
+    And the "Select members from cohort" select box should not contain "System hidden cohort"
+    And the "Select members from cohort" select box should not contain "Cohort in category 2"
+    And the "Select members from cohort" select box should not contain "Cohort empty in category 1"
+    And the "Select members from cohort" select box should not contain "System empty cohort"
+
index 2ca6592..9a70da3 100644 (file)
@@ -19,13 +19,13 @@ Feature: A privileged user can create cohorts using a CSV file
     And I upload "cohort/tests/fixtures/uploadcohorts1.csv" file to "File" filemanager
     And I click on "Preview" "button"
     Then the following should exist in the "previewuploadedcohorts" table:
-      | name          | idnumber  | description       | Context       | Status |
-      | cohort name 1 | cohortid1 | first description | System        |        |
-      | cohort name 2 | cohortid2 |                   | System        |        |
-      | cohort name 3 | cohortid3 |                   | Miscellaneous |        |
-      | cohort name 4 | cohortid4 |                   | Cat 1         |        |
-      | cohort name 5 | cohortid5 |                   | Cat 2         |        |
-      | cohort name 6 | cohortid6 |                   | Cat 3         |        |
+      | name          | idnumber  | description       | Context       | visible | Status |
+      | cohort name 1 | cohortid1 | first description | System        | 1       |        |
+      | cohort name 2 | cohortid2 |                   | System        | 1       |        |
+      | cohort name 3 | cohortid3 |                   | Miscellaneous | 0       |        |
+      | cohort name 4 | cohortid4 |                   | Cat 1         | 1       |        |
+      | cohort name 5 | cohortid5 |                   | Cat 2         | 0       |        |
+      | cohort name 6 | cohortid6 |                   | Cat 3         | 1       |        |
     And I press "Upload cohorts"
     And I should see "Uploaded 6 cohorts"
     And I press "Continue"
@@ -42,6 +42,12 @@ Feature: A privileged user can create cohorts using a CSV file
       | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
       | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
       | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
+    And the "class" attribute of "cohort name 1" "table_row" should not contain "dimmed_text"
+    And the "class" attribute of "cohort name 2" "table_row" should not contain "dimmed_text"
+    And the "class" attribute of "cohort name 3" "table_row" should contain "dimmed_text"
+    And the "class" attribute of "cohort name 4" "table_row" should not contain "dimmed_text"
+    And the "class" attribute of "cohort name 5" "table_row" should contain "dimmed_text"
+    And the "class" attribute of "cohort name 6" "table_row" should not contain "dimmed_text"
 
   @javascript
   Scenario: Upload cohorts with default category context as admin
index ee44107..0b662bf 100644 (file)
@@ -380,7 +380,13 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         cohort_add_member($cohort1->id, $user3->id);
         cohort_add_member($cohort2->id, $user2->id);
 
+        // Cohort1 (Cat1) has 2 users total, 2 user enrolled in course1, 0 users enrolled in course2.
+        // Cohort2 (Cat2) has 1 users total, 0 user enrolled in course1, 1 users enrolled in course2.
+        // Cohort3 (Syst) has 1 users total, 1 user enrolled in course1, 0 users enrolled in course2.
+        // Cohort4 (Syst) has 0 users total.
+
         $list = cohort_get_visible_list($course1);
+        $this->assertDebuggingCalled();
         $this->assertEquals(2, count($list));
         $this->assertNotEmpty($list[$cohort1->id]);
         $this->assertRegExp('/\(2\)$/', $list[$cohort1->id]);
@@ -388,6 +394,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertRegExp('/\(1\)$/', $list[$cohort3->id]);
 
         $list = cohort_get_visible_list($course1, false);
+        $this->assertDebuggingCalled();
         $this->assertEquals(3, count($list));
         $this->assertNotEmpty($list[$cohort1->id]);
         $this->assertRegExp('/\(2\)$/', $list[$cohort1->id]);
@@ -397,11 +404,13 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertRegExp('/[^\)]$/', $list[$cohort4->id]);
 
         $list = cohort_get_visible_list($course2);
+        $this->assertDebuggingCalled();
         $this->assertEquals(1, count($list));
         $this->assertNotEmpty($list[$cohort2->id]);
         $this->assertRegExp('/\(1\)$/', $list[$cohort2->id]);
 
         $list = cohort_get_visible_list($course2, false);
+        $this->assertDebuggingCalled();
         $this->assertEquals(3, count($list));
         $this->assertNotEmpty($list[$cohort2->id]);
         $this->assertRegExp('/\(1\)$/', $list[$cohort2->id]);
@@ -411,9 +420,11 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertRegExp('/[^\)]$/', $list[$cohort4->id]);
 
         $list = cohort_get_visible_list($course3);
+        $this->assertDebuggingCalled();
         $this->assertEquals(0, count($list));
 
         $list = cohort_get_visible_list($course3, false);
+        $this->assertDebuggingCalled();
         $this->assertEquals(2, count($list));
         $this->assertNotEmpty($list[$cohort3->id]);
         $this->assertRegExp('/[^\)]$/', $list[$cohort3->id]);
@@ -543,4 +554,187 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals(array($cohort2->id=>$cohort2), $result['cohorts']);
         $this->assertEquals(2, $result['allcohorts']);
     }
+
+    public function test_cohort_get_available_cohorts() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $category1 = $this->getDataGenerator()->create_category();
+        $category2 = $this->getDataGenerator()->create_category();
+
+        $course1 = $this->getDataGenerator()->create_course(array('category' => $category1->id));
+        $course2 = $this->getDataGenerator()->create_course(array('category' => $category2->id));
+
+        $category1ctx = context_coursecat::instance($category1->id);
+        $category2ctx = context_coursecat::instance($category2->id);
+        $course1ctx = context_course::instance(($course1->id));
+        $course2ctx = context_course::instance(($course2->id));
+        $systemctx = context_system::instance();
+
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid'=>$category1ctx->id, 'name'=>'aaagrrryyy', 'idnumber'=>'','description'=>''));
+        $cohort2 = $this->getDataGenerator()->create_cohort(array('contextid'=>$category1ctx->id, 'name'=>'bbb', 'idnumber'=>'', 'description'=>'yyybrrr', 'visible'=>0));
+        $cohort3 = $this->getDataGenerator()->create_cohort(array('contextid'=>$category2ctx->id, 'name'=>'ccc', 'idnumber'=>'xxarrrghyyy', 'description'=>'po_us'));
+        $cohort4 = $this->getDataGenerator()->create_cohort(array('contextid'=>$systemctx->id, 'name' => 'ddd'));
+        $cohort5 = $this->getDataGenerator()->create_cohort(array('contextid'=>$systemctx->id, 'visible'=>0, 'name' => 'eee'));
+
+        /*
+        Structure of generated course categories, courses and cohort:
+
+        system
+          -cohort4 (visible, has 3 members)
+          -cohort5 (not visible, no members)
+          category1
+            -cohort1 (visible, no members)
+            -cohort2 (not visible, has 1 member)
+            course1
+          category2
+            -cohort3 (visible, has 2 member)
+            course2
+
+        In this test we call cohort_get_available_cohorts() for users with different roles
+        and with different paramteres ($withmembers, $search, $offset, $limit) to make sure we go
+        through all possible options of SQL query.
+        */
+
+        // Admin can see visible and invisible cohorts defined in above contexts.
+        $this->setAdminUser();
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 0, '');
+        $this->assertEquals(array($cohort1->id, $cohort2->id, $cohort4->id, $cohort5->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 2, '');
+        $this->assertEquals(array($cohort1->id, $cohort2->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 1, 2, '');
+        $this->assertEquals(array($cohort2->id, $cohort4->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 100, 'yyy');
+        $this->assertEquals(array($cohort1->id, $cohort2->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course2ctx, COHORT_ALL, 0, 0, '');
+        $this->assertEquals(array($cohort3->id, $cohort4->id, $cohort5->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_MEMBERS_ONLY);
+        $this->assertEmpty($result);
+
+        $result = cohort_get_available_cohorts($course2ctx, COHORT_WITH_MEMBERS_ONLY);
+        $this->assertEmpty($result);
+
+        // Get list of available cohorts as a teacher in the course.
+        $user1 = $this->getDataGenerator()->create_user();
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        role_assign($teacherrole->id, $user1->id, $course1ctx->id);
+        role_assign($teacherrole->id, $user1->id, $course2ctx->id);
+        $this->setUser($user1);
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 0, '');
+        $this->assertEquals(array($cohort1->id, $cohort4->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 1, '');
+        $this->assertEquals(array($cohort1->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 1, 1, '');
+        $this->assertEquals(array($cohort4->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 100, 'yyy');
+        $this->assertEquals(array($cohort1->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course2ctx, COHORT_ALL, 0, 0, '');
+        $this->assertEquals(array($cohort3->id, $cohort4->id), array_keys($result));
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_MEMBERS_ONLY);
+        $this->assertEmpty($result);
+
+        // Now add members to cohorts.
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+        $user6 = $this->getDataGenerator()->create_user();
+        cohort_add_member($cohort2->id, $user3->id);
+        cohort_add_member($cohort3->id, $user2->id);
+        cohort_add_member($cohort3->id, $user3->id);
+        cohort_add_member($cohort4->id, $user4->id);
+        cohort_add_member($cohort4->id, $user5->id);
+        cohort_add_member($cohort4->id, $user6->id);
+
+        // Check filtering non-empty cohorts as admin.
+        $this->setAdminUser();
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort2->id, $cohort4->id), array_keys($result));
+        $this->assertEquals(1, $result[$cohort2->id]->memberscnt);
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course2ctx, COHORT_WITH_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort3->id, $cohort4->id), array_keys($result));
+        $this->assertEquals(2, $result[$cohort3->id]->memberscnt);
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_MEMBERS_ONLY, 0, 0, 'yyy');
+        $this->assertEquals(array($cohort2->id), array_keys($result));
+        $this->assertEquals(1, $result[$cohort2->id]->memberscnt);
+
+        // Check filtering non-empty cohorts as teacher.
+        $this->setUser($user1);
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort4->id), array_keys($result));
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course2ctx, COHORT_WITH_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort3->id, $cohort4->id), array_keys($result));
+        $this->assertEquals(2, $result[$cohort3->id]->memberscnt);
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_MEMBERS_ONLY, 0, 0, 'yyy');
+        $this->assertEmpty($result);
+
+        // Enrol users.
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user5->id, $course1->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user6->id, $course1->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course2->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course2->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user5->id, $course2->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user6->id, $course2->id, $studentrole->id);
+
+        // Check cohorts with enrolments as admin.
+        $this->setAdminUser();
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_ENROLLED_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort2->id, $cohort4->id), array_keys($result));
+        $this->assertEquals(1, $result[$cohort2->id]->enrolledcnt);
+        $this->assertEquals(2, $result[$cohort4->id]->enrolledcnt);
+        $this->assertEquals(1, $result[$cohort2->id]->memberscnt);
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course2ctx, COHORT_WITH_ENROLLED_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort3->id, $cohort4->id), array_keys($result));
+        $this->assertEquals(1, $result[$cohort3->id]->enrolledcnt);
+        $this->assertEquals(3, $result[$cohort4->id]->enrolledcnt);
+        $this->assertEquals(2, $result[$cohort3->id]->memberscnt);
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_ENROLLED_MEMBERS_ONLY, 0, 0, 'yyy');
+        $this->assertEquals(array($cohort2->id), array_keys($result));
+        $this->assertEquals(1, $result[$cohort2->id]->enrolledcnt);
+        $this->assertEquals(1, $result[$cohort2->id]->memberscnt);
+
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY, 0, 0, '');
+        $this->assertEquals(array($cohort4->id), array_keys($result));
+        $this->assertEquals(2, $result[$cohort4->id]->enrolledcnt);
+        $this->assertEquals(3, $result[$cohort4->id]->memberscnt);
+
+        // Assign user1 additional 'manager' role in the category context. He can now see hidden cohort in category1
+        // but still can not see hidden category in system.
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
+        role_assign($managerrole->id, $user1->id, context_coursecat::instance($category1->id));
+        $this->setUser($user1);
+        $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 0, '');
+        $this->assertEquals(array($cohort1->id, $cohort2->id, $cohort4->id), array_keys($result));
+    }
 }
index 8f7f4ba..0b0f314 100644 (file)
@@ -41,9 +41,10 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
 
         $contextid = context_system::instance()->id;
+        $category = $this->getDataGenerator()->create_category();
 
         $cohort1 = array(
-            'categorytype' => array('type' => 'id', 'value' => '1'),
+            'categorytype' => array('type' => 'id', 'value' => $category->id),
             'name' => 'cohort test 1',
             'idnumber' => 'cohorttest1',
             'description' => 'This is a description for cohorttest1'
@@ -53,11 +54,12 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             'categorytype' => array('type' => 'system', 'value' => ''),
             'name' => 'cohort test 2',
             'idnumber' => 'cohorttest2',
-            'description' => 'This is a description for cohorttest2'
+            'description' => 'This is a description for cohorttest2',
+            'visible' => 0
             );
 
         $cohort3 = array(
-            'categorytype' => array('type' => 'id', 'value' => '1'),
+            'categorytype' => array('type' => 'id', 'value' => $category->id),
             'name' => 'cohort test 3',
             'idnumber' => 'cohorttest3',
             'description' => 'This is a description for cohorttest3'
@@ -65,21 +67,31 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $roleid = $this->assignUserCapability('moodle/cohort:manage', $contextid);
 
         // Call the external function.
+        $this->setCurrentTimeStart();
         $createdcohorts = core_cohort_external::create_cohorts(array($cohort1, $cohort2));
 
         // Check we retrieve the good total number of created cohorts + no error on capability.
         $this->assertEquals(2, count($createdcohorts));
 
         foreach ($createdcohorts as $createdcohort) {
+            $dbcohort = $DB->get_record('cohort', array('id' => $createdcohort['id']));
             if ($createdcohort['idnumber'] == $cohort1['idnumber']) {
-                $dbcohort = $DB->get_record('cohort', array('id' => $createdcohort['id']));
                 $conid = $DB->get_field('context', 'id', array('instanceid' => $cohort1['categorytype']['value'],
                         'contextlevel' => CONTEXT_COURSECAT));
                 $this->assertEquals($dbcohort->contextid, $conid);
                 $this->assertEquals($dbcohort->name, $cohort1['name']);
-                $this->assertEquals($dbcohort->idnumber, $cohort1['idnumber']);
                 $this->assertEquals($dbcohort->description, $cohort1['description']);
+                $this->assertEquals($dbcohort->visible, 1); // Field was not specified, ensure it is visible by default.
+            } else if ($createdcohort['idnumber'] == $cohort2['idnumber']) {
+                $this->assertEquals($dbcohort->contextid, context_system::instance()->id);
+                $this->assertEquals($dbcohort->name, $cohort2['name']);
+                $this->assertEquals($dbcohort->description, $cohort2['description']);
+                $this->assertEquals($dbcohort->visible, $cohort2['visible']);
+            } else {
+                $this->fail('Unrecognised cohort found');
             }
+            $this->assertTimeCurrent($dbcohort->timecreated);
+            $this->assertTimeCurrent($dbcohort->timemodified);
         }
 
         // Call without required capability.
@@ -151,6 +163,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             if ($enrolledcohort['idnumber'] == $cohort1->idnumber) {
                 $this->assertEquals($cohort1->name, $enrolledcohort['name']);
                 $this->assertEquals($cohort1->description, $enrolledcohort['description']);
+                $this->assertEquals($cohort1->visible, $enrolledcohort['visible']);
             }
         }
 
@@ -173,7 +186,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $cohort1 = self::getDataGenerator()->create_cohort();
+        $cohort1 = self::getDataGenerator()->create_cohort(array('visible' => 0));
 
         $cohort1 = array(
             'id' => $cohort1->id,
@@ -196,6 +209,15 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($dbcohort->name, $cohort1['name']);
         $this->assertEquals($dbcohort->idnumber, $cohort1['idnumber']);
         $this->assertEquals($dbcohort->description, $cohort1['description']);
+        $this->assertEquals($dbcohort->visible, 0);
+
+        // Since field 'visible' was added in 2.8, make sure that update works correctly with and without this parameter.
+        core_cohort_external::update_cohorts(array($cohort1 + array('visible' => 1)));
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $this->assertEquals(1, $dbcohort->visible);
+        core_cohort_external::update_cohorts(array($cohort1));
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $this->assertEquals(1, $dbcohort->visible);
 
         // Call without required capability.
         $this->unassignUserCapability('moodle/cohort:manage', $context->id, $roleid);
index b9ed12f..8fe77ed 100644 (file)
@@ -1,7 +1,7 @@
-name,idnumber,description,category
-cohort name 1,cohortid1,first description,
-cohort name 2,cohortid2,,
-cohort name 3,cohortid3,,Miscellaneous
-cohort name 4,cohortid4,,CAT1
-cohort name 5,cohortid5,,CAT2
-cohort name 6,cohortid6,,CAT3
+name,idnumber,description,category,visible
+cohort name 1,cohortid1,first description,,
+cohort name 2,cohortid2,,,
+cohort name 3,cohortid3,,Miscellaneous,no
+cohort name 4,cohortid4,,CAT1,yes
+cohort name 5,cohortid5,,CAT2,0
+cohort name 6,cohortid6,,CAT3,1
index fa041aa..a7ee3a8 100644 (file)
@@ -359,7 +359,7 @@ class cohort_upload_form extends moodleform {
         $columns = $cir->get_columns();
 
         // Check that columns include 'name' and warn about extra columns.
-        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat');
+        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat', 'visible');
         $additionalcolumns = array('context', 'category', 'category_id', 'category_idnumber', 'category_path');
         $displaycolumns = array();
         $extracolumns = array();
@@ -453,6 +453,19 @@ class cohort_upload_form extends moodleform {
                 case 'idnumber': $hash[$key] = core_text::substr(clean_param($value, PARAM_RAW), 0, 254); break;
                 case 'description': $hash[$key] = clean_param($value, PARAM_RAW); break;
                 case 'descriptionformat': $hash[$key] = clean_param($value, PARAM_INT); break;
+                case 'visible':
+                    $tempstr = trim(core_text::strtolower($value));
+                    if ($tempstr === '') {
+                        // Empty string is treated as "YES" (the default value for cohort visibility).
+                        $hash[$key] = 1;
+                    } else {
+                        if ($tempstr === core_text::strtolower(get_string('no')) || $tempstr === 'n') {
+                            // Special treatment for 'no' string that is not included in clean_param().
+                            $value = 0;
+                        }
+                        $hash[$key] = clean_param($value, PARAM_BOOL) ? 1 : 0;
+                    }
+                    break;
             }
         }
     }
index 14de49b..f13757b 100644 (file)
@@ -804,8 +804,7 @@ class helper {
         }
 
         list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
-        $params['categoryid'] = $moveto->id;
-        $sql = "SELECT c.id, c.category FROM {course} c WHERE c.id {$where} AND c.category <> :categoryid";
+        $sql = "SELECT c.id, c.category FROM {course} c WHERE c.id {$where}";
         $courses = $DB->get_records_sql($sql, $params);
         $checks = array();
         foreach ($courseids as $id) {
index c48905c..6f02f93 100644 (file)
@@ -786,6 +786,31 @@ class core_course_management_renderer extends plugin_renderer_base {
         return $html;
     }
 
+    /**
+     * Renderers bulk actions that can be performed on courses in search returns
+     *
+     * @return string
+     */
+    public function course_search_bulk_actions() {
+        $html  = html_writer::start_div('course-bulk-actions bulk-actions');
+        $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
+        $options = coursecat::make_categories_list('moodle/category:manage');
+        $select = html_writer::select(
+            $options,
+            'movecoursesto',
+            '',
+            array('' => 'choosedots'),
+            array('aria-labelledby' => 'moveselectedcoursesto')
+        );
+        $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'));
+        $html .= $this->detail_pair(
+            html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
+            $select . html_writer::empty_tag('input', $submit)
+        );
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
     /**
      * Renderers detailed course information.
      *
@@ -1085,6 +1110,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
         $html .= html_writer::end_tag('ul');
         $html .= $this->search_pagination($totalcourses, $page, $perpage, true, $search);
+        $html .= $this->course_search_bulk_actions();
         $html .= html_writer::end_div();
         return $html;
     }
@@ -1173,15 +1199,26 @@ class core_course_management_renderer extends plugin_renderer_base {
             'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
             'data-visible' => $course->visible ? '1' : '0'
         );
-
-        $bulkcourseinput = array('type' => 'checkbox', 'name' => 'bc[]', 'value' => $course->id, 'class' => 'bulk-action-checkbox');
+        $bulkcourseinput = '';
+        if (coursecat::get($course->category)->can_move_courses_out_of()) {
+            $bulkcourseinput = array(
+                'type' => 'checkbox',
+                'name' => 'bc[]',
+                'value' => $course->id,
+                'class' => 'bulk-action-checkbox',
+                'aria-label' => get_string('bulkactionselect', 'moodle', $text),
+                'data-action' => 'select'
+            );
+        }
         $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
         $categoryname = coursecat::get($course->category)->get_formatted_name();
 
         $html  = html_writer::start_tag('li', $attributes);
         $html .= html_writer::start_div('clearfix');
         $html .= html_writer::start_div('float-left');
-        $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        if ($bulkcourseinput) {
+            $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        }
         $html .= html_writer::end_div();
         $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
         $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left categoryname'));
index db506fb..8c4d6ff 100644 (file)
@@ -290,7 +290,7 @@ if ($action !== false && confirm_sesskey()) {
                 $moveto = coursecat::get($movetoid);
                 try {
                     // If this fails we want to catch the exception and report it.
-                    $redirectback = \core_course\management\helper::action_category_move_courses_into($category, $moveto,
+                    $redirectback = \core_course\management\helper::move_courses_into_category($moveto,
                         $courseids);
                     if ($redirectback) {
                         $a = new stdClass;
index 55d242f..dab59f8 100644 (file)
@@ -1676,13 +1676,12 @@ class core_course_renderer extends plugin_renderer_base {
         $site = get_site();
         $output = '';
 
-        $this->page->set_button($this->course_search_form('', 'navbar'));
+        if (can_edit_in_category($category)) {
+            // Add 'Manage' button if user has permissions to edit this category.
+            $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
+            $this->page->set_button($managebutton);
+        }
         if (!$coursecat->id) {
-            if (can_edit_in_category()) {
-                // add 'Manage' button instead of course search form
-                $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
-                $this->page->set_button($managebutton);
-            }
             if (coursecat::count_all() == 1) {
                 // There exists only one category in the system, do not display link to it
                 $coursecat = coursecat::get_default();
@@ -1746,15 +1745,12 @@ class core_course_renderer extends plugin_renderer_base {
             $catdisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'categories', 'page' => 1));
         }
         $chelper->set_courses_display_options($coursedisplayoptions)->set_categories_display_options($catdisplayoptions);
+        // Add course search form.
+        $output .= $this->course_search_form();
 
-        // Display course category tree
+        // Display course category tree.
         $output .= $this->coursecat_tree($chelper, $coursecat);
 
-        // Add course search form (if we are inside category it was already added to the navbar)
-        if (!$coursecat->id) {
-            $output .= $this->course_search_form();
-        }
-
         // Add action buttons
         $output .= $this->container_start('buttons');
         $context = get_category_or_system_context($coursecat->id);
index 622909b..52ba8fa 100644 (file)
@@ -1,10 +1,37 @@
 <?php
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+// 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/>.
+
+/**
+ * Provides the course_reset_form class.
+ *
+ * @package     core
+ * @copyright   2007 Petr Skoda
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
 
 require_once $CFG->libdir.'/formslib.php';
 
+/**
+ * Defines the course reset settings form.
+ *
+ * @copyright   2007 Petr Skoda
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class course_reset_form extends moodleform {
     function definition (){
         global $CFG, $COURSE, $DB;
diff --git a/course/tests/behat/course_search.feature b/course/tests/behat/course_search.feature
new file mode 100644 (file)
index 0000000..24bfaad
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_course
+Feature: Courses can be searched for and moved in bulk.
+  In order to manage a large number of courses
+  As a Moodle Administrator
+  I need to be able to search courses in bulk and move them around
+
+  Background:
+     Given the following "categories" exist:
+      | name | category | idnumber |
+      | Science | 0 | SCI |
+      | English | 0 | ENG |
+      | Miscellaneous | 0 | MISC |
+
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Biology Y1 | BIO1 | MISC |
+      | Biology Y2 | BIO2 | MISC |
+      | English Y1 | ENG1 | ENG |
+      | English Y2 | ENG2 | MISC |
+
+  Scenario: Search courses finds correct results
+    Given I log in as "admin"
+    And I go to the courses management page
+    When I set the field "Search courses:" to "Biology"
+    And I press "Go"
+    Then I should see "Biology Y1"
+    And I should see "Biology Y2"
+    And I should not see "English Y1"
+    And I should not see "English Y2"
+
+  @javascript
+  Scenario: Search courses and move results in bulk
+    Given I log in as "admin"
+    And I go to the courses management page
+    And I set the field "Search courses:" to "Biology"
+    And I press "Go"
+    When I select course "Biology Y1" in the management interface
+    And I select course "Biology Y2" in the management interface
+    And I set the field "menumovecoursesto" to "Science"
+    And I press "Move"
+    Then I should see "Successfully moved 2 courses into Science"
+    And I wait to be redirected
+    And I click on category "Science" in the management interface
+    And I should see "Biology Y1"
+    And I should see "Biology Y2"
diff --git a/enrol/cohort/ajax.php b/enrol/cohort/ajax.php
deleted file mode 100644 (file)
index e987f87..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file processes AJAX enrolment actions and returns JSON for the cohort plugin
- *
- * The general idea behind this file is that any errors should throw exceptions
- * which will be returned and acted upon by the calling AJAX script.
- *
- * @package    enrol_cohort
- * @copyright  2011 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define('AJAX_SCRIPT', true);
-
-require('../../config.php');
-require_once($CFG->dirroot.'/enrol/locallib.php');
-require_once($CFG->dirroot.'/enrol/cohort/locallib.php');
-require_once($CFG->dirroot.'/group/lib.php');
-
-// Must have the sesskey.
-$id      = required_param('id', PARAM_INT); // course id
-$action  = required_param('action', PARAM_ALPHANUMEXT);
-
-$PAGE->set_url(new moodle_url('/enrol/cohort/ajax.php', array('id'=>$id, 'action'=>$action)));
-
-$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
-$context = context_course::instance($course->id, MUST_EXIST);
-
-if ($course->id == SITEID) {
-    throw new moodle_exception('invalidcourse');
-}
-
-require_login($course);
-require_capability('moodle/course:enrolreview', $context);
-require_sesskey();
-
-if (!enrol_is_enabled('cohort')) {
-    // This should never happen, no need to invent new error strings.
-    throw new enrol_ajax_exception('errorenrolcohort');
-}
-
-echo $OUTPUT->header(); // Send headers.
-
-$manager = new course_enrolment_manager($PAGE, $course);
-
-$outcome = new stdClass();
-$outcome->success = true;
-$outcome->response = new stdClass();
-$outcome->error = '';
-
-switch ($action) {
-    case 'getassignable':
-        $otheruserroles = optional_param('otherusers', false, PARAM_BOOL);
-        $outcome->response = array_reverse($manager->get_assignable_roles($otheruserroles), true);
-        break;
-    case 'getdefaultcohortrole': //TODO: use in ajax UI MDL-24280
-        $cohortenrol = enrol_get_plugin('cohort');
-        $outcome->response = $cohortenrol->get_config('roleid');
-        break;
-    case 'getcohorts':
-        require_capability('moodle/course:enrolconfig', $context);
-        $offset = optional_param('offset', 0, PARAM_INT);
-        $search  = optional_param('search', '', PARAM_RAW);
-        $outcome->response = enrol_cohort_search_cohorts($manager, $offset, 25, $search);
-        // Some browsers reorder collections by key.
-        $outcome->response['cohorts'] = array_values($outcome->response['cohorts']);
-        break;
-    case 'enrolcohort':
-        require_capability('moodle/course:enrolconfig', $context);
-        require_capability('enrol/cohort:config', $context);
-        $roleid = required_param('roleid', PARAM_INT);
-        $cohortid = required_param('cohortid', PARAM_INT);
-
-        $roles = $manager->get_assignable_roles();
-        if (!enrol_cohort_can_view_cohort($cohortid) || !array_key_exists($roleid, $roles)) {
-            throw new enrol_ajax_exception('errorenrolcohort');
-        }
-        $enrol = enrol_get_plugin('cohort');
-        $enrol->add_instance($manager->get_course(), array('customint1' => $cohortid, 'roleid' => $roleid));
-        $trace = new null_progress_trace();
-        enrol_cohort_sync($trace, $manager->get_course()->id);
-        $trace->finished();
-        break;
-    case 'enrolcohortusers':
-        //TODO: this should be moved to enrol_manual, see MDL-35618.
-        require_capability('enrol/manual:enrol', $context);
-        $roleid = required_param('roleid', PARAM_INT);
-        $cohortid = required_param('cohortid', PARAM_INT);
-
-        $roles = $manager->get_assignable_roles();
-        if (!enrol_cohort_can_view_cohort($cohortid) || !array_key_exists($roleid, $roles)) {
-            throw new enrol_ajax_exception('errorenrolcohort');
-        }
-
-        $result = enrol_cohort_enrol_all_users($manager, $cohortid, $roleid);
-        if ($result === false) {
-            throw new enrol_ajax_exception('errorenrolcohortusers');
-        }
-
-        $outcome->success = true;
-        $outcome->response->users = $result;
-        $outcome->response->title = get_string('success');
-        $outcome->response->message = get_string('enrollednewusers', 'enrol', $result);
-        $outcome->response->yesLabel = get_string('ok');
-        break;
-    default:
-        throw new enrol_ajax_exception('unknowajaxaction');
-}
-
-echo json_encode($outcome);
-die();
index 7f3011f..21ec3d6 100644 (file)
@@ -65,20 +65,10 @@ class enrol_cohort_edit_form extends moodleform {
 
         } else {
             $cohorts = array('' => get_string('choosedots'));
-            list($sqlparents, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids());
-            $sql = "SELECT id, name, idnumber, contextid
-                      FROM {cohort}
-                     WHERE contextid $sqlparents
-                  ORDER BY name ASC, idnumber ASC";
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $c) {
-                $context = context::instance_by_id($c->contextid);
-                if (!has_capability('moodle/cohort:view', $context)) {
-                    continue;
-                }
+            $allcohorts = cohort_get_available_cohorts($coursecontext);
+            foreach ($allcohorts as $c) {
                 $cohorts[$c->id] = format_string($c->name);
             }
-            $rs->close();
             $mform->addElement('select', 'customint1', get_string('cohort', 'cohort'), $cohorts);
             $mform->addRule('customint1', get_string('required'), 'required', null, 'client');
         }
index 97c3fa4..48550f2 100644 (file)
@@ -23,9 +23,7 @@
  */
 
 $string['addgroup'] = 'Add to group';
-$string['ajaxmore'] = 'More...';
 $string['assignrole'] = 'Assign role';
-$string['cohortsearch'] = 'Search';
 $string['cohort:config'] = 'Configure cohort instances';
 $string['cohort:unenrol'] = 'Unenrol suspended users';
 $string['instanceexists'] = 'Cohort is already synchronised with selected role';
index 9146b21..0f338fb 100644 (file)
@@ -95,25 +95,13 @@ class enrol_cohort_plugin extends enrol_plugin {
      * @return bool
      */
     protected function can_add_new_instances($courseid) {
-        global $DB;
-
+        global $CFG;
+        require_once($CFG->dirroot . '/cohort/lib.php');
         $coursecontext = context_course::instance($courseid);
         if (!has_capability('moodle/course:enrolconfig', $coursecontext) or !has_capability('enrol/cohort:config', $coursecontext)) {
             return false;
         }
-        list($sqlparents, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids());
-        $sql = "SELECT id, contextid
-                  FROM {cohort}
-                 WHERE contextid $sqlparents
-              ORDER BY name ASC";
-        $cohorts = $DB->get_records_sql($sql, $params);
-        foreach ($cohorts as $c) {
-            $context = context::instance_by_id($c->contextid);
-            if (has_capability('moodle/cohort:view', $context)) {
-                return true;
-            }
-        }
-        return false;
+        return cohort_get_available_cohorts($coursecontext) ? true : false;
     }
 
     /**
@@ -220,54 +208,6 @@ class enrol_cohort_plugin extends enrol_plugin {
         return $actions;
     }
 
-    /**
-     * Returns a button to enrol a cohort or its users through the manual enrolment plugin.
-     *
-     * This function also adds a quickenrolment JS ui to the page so that users can be enrolled
-     * via AJAX.
-     *
-     * @param course_enrolment_manager $manager
-     * @return enrol_user_button
-     */
-    public function get_manual_enrol_button(course_enrolment_manager $manager) {
-        $course = $manager->get_course();
-        if (!$this->can_add_new_instances($course->id)) {
-            return false;
-        }
-
-        $cohorturl = new moodle_url('/enrol/cohort/edit.php', array('courseid' => $course->id));
-        $button = new enrol_user_button($cohorturl, get_string('enrolcohort', 'enrol'), 'get');
-        $button->class .= ' enrol_cohort_plugin';
-
-        $button->strings_for_js(array(
-            'enrol',
-            'synced',
-            'enrolcohort',
-            'enrolcohortusers',
-            ), 'enrol');
-        $button->strings_for_js(array(
-            'ajaxmore',
-            'cohortsearch',
-            ), 'enrol_cohort');
-        $button->strings_for_js('assignroles', 'role');
-        $button->strings_for_js('cohort', 'cohort');
-        $button->strings_for_js('users', 'moodle');
-
-        // No point showing this at all if the user cant manually enrol users.
-        $hasmanualinstance = has_capability('enrol/manual:enrol', $manager->get_context()) && $manager->has_instance('manual');
-
-        $modules = array('moodle-enrol_cohort-quickenrolment', 'moodle-enrol_cohort-quickenrolment-skin');
-        $function = 'M.enrol_cohort.quickenrolment.init';
-        $arguments = array(
-            'courseid'        => $course->id,
-            'ajaxurl'         => '/enrol/cohort/ajax.php',
-            'url'             => $manager->get_moodlepage()->url->out(false),
-            'manualEnrolment' => $hasmanualinstance);
-        $button->require_yui_module($modules, $function, array($arguments));
-
-        return $button;
-    }
-
     /**
      * Restore instance and map settings.
      *
index ec9f9cf..35f1e03 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot . '/enrol/locallib.php');
+require_once($CFG->dirroot . '/cohort/lib.php');
 
 
 /**
@@ -327,186 +328,3 @@ function enrol_cohort_sync(progress_trace $trace, $courseid = NULL) {
 
     return 0;
 }
-
-/**
- * Enrols all of the users in a cohort through a manual plugin instance.
- *
- * In order for this to succeed the course must contain a valid manual
- * enrolment plugin instance that the user has permission to enrol users through.
- *
- * @global moodle_database $DB
- * @param course_enrolment_manager $manager
- * @param int $cohortid
- * @param int $roleid
- * @return int
- */
-function enrol_cohort_enrol_all_users(course_enrolment_manager $manager, $cohortid, $roleid) {
-    global $DB;
-    $context = $manager->get_context();
-    require_capability('moodle/course:enrolconfig', $context);
-
-    $instance = false;
-    $instances = $manager->get_enrolment_instances();
-    foreach ($instances as $i) {
-        if ($i->enrol == 'manual') {
-            $instance = $i;
-            break;
-        }
-    }
-    $plugin = enrol_get_plugin('manual');
-    if (!$instance || !$plugin || !$plugin->allow_enrol($instance) || !has_capability('enrol/'.$plugin->get_name().':enrol', $context)) {
-        return false;
-    }
-    $sql = "SELECT com.userid
-              FROM {cohort_members} com
-         LEFT JOIN (
-                SELECT *
-                  FROM {user_enrolments} ue
-                 WHERE ue.enrolid = :enrolid
-                 ) ue ON ue.userid=com.userid
-             WHERE com.cohortid = :cohortid AND ue.id IS NULL";
-    $params = array('cohortid' => $cohortid, 'enrolid' => $instance->id);
-    $rs = $DB->get_recordset_sql($sql, $params);
-    $count = 0;
-    foreach ($rs as $user) {
-        $count++;
-        $plugin->enrol_user($instance, $user->userid, $roleid);
-    }
-    $rs->close();
-    return $count;
-}
-
-/**
- * Gets all the cohorts the user is able to view.
- *
- * @global moodle_database $DB
- * @param course_enrolment_manager $manager
- * @return array
- */
-function enrol_cohort_get_cohorts(course_enrolment_manager $manager) {
-    global $DB;
-    $context = $manager->get_context();
-    $cohorts = array();
-    $instances = $manager->get_enrolment_instances();
-    $enrolled = array();
-    foreach ($instances as $instance) {
-        if ($instance->enrol == 'cohort') {
-            $enrolled[] = $instance->customint1;
-        }
-    }
-    list($sqlparents, $params) = $DB->get_in_or_equal($context->get_parent_context_ids());
-    $sql = "SELECT id, name, idnumber, contextid
-              FROM {cohort}
-             WHERE contextid $sqlparents
-          ORDER BY name ASC, idnumber ASC";
-    $rs = $DB->get_recordset_sql($sql, $params);
-    foreach ($rs as $c) {
-        $context = context::instance_by_id($c->contextid);
-        if (!has_capability('moodle/cohort:view', $context)) {
-            continue;
-        }
-        $cohorts[$c->id] = array(
-            'cohortid'=>$c->id,
-            'name'=>format_string($c->name, true, array('context'=>context::instance_by_id($c->contextid))),
-            'users'=>$DB->count_records('cohort_members', array('cohortid'=>$c->id)),
-            'enrolled'=>in_array($c->id, $enrolled)
-        );
-    }
-    $rs->close();
-    return $cohorts;
-}
-
-/**
- * Check if cohort exists and user is allowed to enrol it.
- *
- * @global moodle_database $DB
- * @param int $cohortid Cohort ID
- * @return boolean
- */
-function enrol_cohort_can_view_cohort($cohortid) {
-    global $DB;
-    $cohort = $DB->get_record('cohort', array('id' => $cohortid), 'id, contextid');
-    if ($cohort) {
-        $context = context::instance_by_id($cohort->contextid);
-        if (has_capability('moodle/cohort:view', $context)) {
-            return true;
-        }
-    }
-    return false;
-}
-
-/**
- * Gets cohorts the user is able to view.
- *
- * @global moodle_database $DB
- * @param course_enrolment_manager $manager
- * @param int $offset limit output from
- * @param int $limit items to output per load
- * @param string $search search string
- * @return array    Array(more => bool, offset => int, cohorts => array)
- */
-function enrol_cohort_search_cohorts(course_enrolment_manager $manager, $offset = 0, $limit = 25, $search = '') {
-    global $DB;
-    $context = $manager->get_context();
-    $cohorts = array();
-    $instances = $manager->get_enrolment_instances();
-    $enrolled = array();
-    foreach ($instances as $instance) {
-        if ($instance->enrol == 'cohort') {
-            $enrolled[] = $instance->customint1;
-        }
-    }
-
-    list($sqlparents, $params) = $DB->get_in_or_equal($context->get_parent_context_ids());
-
-    // Add some additional sensible conditions.
-    $tests = array('contextid ' . $sqlparents);
-
-    // Modify the query to perform the search if required.
-    if (!empty($search)) {
-        $conditions = array(
-            'name',
-            'idnumber',
-            'description'
-        );
-        $searchparam = '%' . $DB->sql_like_escape($search) . '%';
-        foreach ($conditions as $key=>$condition) {
-            $conditions[$key] = $DB->sql_like($condition, "?", false);
-            $params[] = $searchparam;
-        }
-        $tests[] = '(' . implode(' OR ', $conditions) . ')';
-    }
-    $wherecondition = implode(' AND ', $tests);
-
-    $sql = "SELECT id, name, idnumber, contextid, description
-              FROM {cohort}
-             WHERE $wherecondition
-          ORDER BY name ASC, idnumber ASC";
-    $rs = $DB->get_recordset_sql($sql, $params, $offset);
-
-    // Produce the output respecting parameters.
-    foreach ($rs as $c) {
-        // Track offset.
-        $offset++;
-        // Check capabilities.
-        $context = context::instance_by_id($c->contextid);
-        if (!has_capability('moodle/cohort:view', $context)) {
-            continue;
-        }
-        if ($limit === 0) {
-            // We have reached the required number of items and know that there are more, exit now.
-            $offset--;
-            break;
-        }
-        $cohorts[$c->id] = array(
-            'cohortid' => $c->id,
-            'name'     => shorten_text(format_string($c->name, true, array('context'=>context::instance_by_id($c->contextid))), 35),
-            'users'    => $DB->count_records('cohort_members', array('cohortid'=>$c->id)),
-            'enrolled' => in_array($c->id, $enrolled)
-        );
-        // Count items.
-        $limit--;
-    }
-    $rs->close();
-    return array('more' => !(bool)$limit, 'offset' => $offset, 'cohorts' => $cohorts);
-}
diff --git a/enrol/cohort/yui/quickenrolment/assets/skins/sam/quickenrolment.css b/enrol/cohort/yui/quickenrolment/assets/skins/sam/quickenrolment.css
deleted file mode 100644 (file)
index ae247a0..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-.qce-panel {background-color:#666;border:2px solid #666;border-width:0 2px 2px 0;min-width:420px;}
-.qce-panel .yui3-widget-hd {background:url("sprite.png");background-repeat:repeat-x;background-color:#DDD;background-position: 0 -15px;border-bottom:1px solid #555;border-top:1px solid #fff;}
-.qce-panel .yui3-widget-hd h2 {margin:3px 5px 2px;padding:0;font-size:110%;}
-.qce-panel .yui3-widget-hd .close {width:25px;height:15px;position:absolute;top:3px;right:1em;cursor:pointer;background:url("sprite.png") no-repeat scroll 0 0 transparent;}
-.dir-rtl .qce-panel .yui3-widget-hd .close {right:auto;left:1em;}
-.qce-panel .yui3-overlay-content {background-color:#F6F6F6;border:1px solid #555;margin-top:-2px;margin-left:-2px;}
-.qce-panel .qce-enrollable-cohorts {margin:5px;}
-.qce-panel .qce-cohorts {border:1px solid #666;min-width:408px;background-color:#FFF;height:375px;overflow:auto;}
-.qce-panel .qce-cohorts .qce-more-results {background-color:#eee;padding:5px;border-top:1px solid #BBB;text-align:center;}
-
-.qce-panel .qce-panel-content {min-height:405px;}
-.qce-panel .qce-panel-content .qce-loading-lightbox {position:absolute;width:100%;height:100%;top:0;left:0;background-color:#FFF;min-height:264px;text-align:center;}
-.qce-panel .qce-panel-content .qce-loading-lightbox.hidden {display:none;}
-.qce-panel .qce-panel-content .qce-loading-lightbox .loading-icon {margin:auto;vertical-align:middle;margin-top:198px;}
-
-.qce-panel .qce-cohort {width:100%;position:relative;clear:both;height:24px;white-space:nowrap;}
-.qce-panel .qce-cohort div {display:inline-block;overflow:hidden;}
-.qce-panel .qce-cohort.odd {background-color:#f4f4f4;}
-.qce-panel .qce-cohort .qce-cohort-button {text-align:center;display:inline-block;font-size:80%;height:22px;line-height:22px;overflow:hidden;padding:0 4px;}
-.qce-panel .qce-cohort .qce-cohort-button.notenrolled {background-color:#ddd;border:1px outset #CCC;background:url("sprite.png");background-repeat:repeat-x;background-color:#DDD;background-position: 0 -25px;color:inherit;}
-.qce-panel .qce-cohort .qce-cohort-button.notenrolled:hover {background-position:0 -15px;cursor:pointer;}
-.qce-panel .qce-cohort .qce-cohort-button.notenrolled.enrolusers {margin-right: 4px;}
-.qce-panel .qce-cohort .qce-cohort-button.alreadyenrolled {font-weight:bold;}
-.qce-panel .qce-cohort .qce-cohort-name {line-height:24px;width:220px;}
-.qce-panel .canenrolusers .qce-cohort .qce-cohort-button.alreadyenrolled {width:125px;}
-.qce-panel .canenrolusers .qce-cohort .qce-cohort-name {line-height:24px;}
-.qce-panel .qce-cohort .qce-cohort-users {width:30px;text-align:right;height:24px;line-height:24px;margin-right:4px}
-
-.qce-panel .qce-assignable-roles {margin:5px 6px 0 6px;}
-.qce-panel .qce-assignable-roles label {padding-right:8px;}
-
-.qce-panel .qce-footer {margin:0 6px 5px 6px;}
-.qce-panel .qce-search label {padding-right:8px;}
-.qce-panel .qce-search input {width:70%;}
-
-.qce-panel .qce-cohort.headings {font-weight:bold;border-width:0;}
-.qe-panel .qce-cohort.headings .qce-cohort-button {display:none;}
-
-.ie7 .qce-panel .qce-cohort div,
-.ie7 .qce-panel .canenrolusers .qce-cohort .qce-cohort-name {float:left;}
diff --git a/enrol/cohort/yui/quickenrolment/assets/skins/sam/sprite.png b/enrol/cohort/yui/quickenrolment/assets/skins/sam/sprite.png
deleted file mode 100644 (file)
index 067c225..0000000
Binary files a/enrol/cohort/yui/quickenrolment/assets/skins/sam/sprite.png and /dev/null differ
diff --git a/enrol/cohort/yui/quickenrolment/quickenrolment.js b/enrol/cohort/yui/quickenrolment/quickenrolment.js
deleted file mode 100644 (file)
index 661af04..0000000
+++ /dev/null
@@ -1,396 +0,0 @@
-YUI.add('moodle-enrol_cohort-quickenrolment', function(Y) {
-
-    var CONTROLLERNAME = 'Quick cohort enrolment controller',
-        COHORTNAME = 'Cohort',
-        COHORTID = 'cohortid',
-        ENROLLED = 'enrolled',
-        NAME = 'name',
-        USERS = 'users',
-        COURSEID = 'courseid',
-        ASSIGNABLEROLES = 'assignableRoles',
-        DEFAULTCOHORTROLE = 'defaultCohortRole',
-        COHORTS = 'cohorts',
-        MORERESULTS = 'moreresults',
-        FIRSTPAGE = 'firstpage',
-        OFFSET = 'offset',
-        PANELID = 'qce-panel-',
-        REQUIREREFRESH = 'requiresRefresh',
-        SEARCH = 'search',
-        URL = 'url',
-        AJAXURL = 'ajaxurl',
-        MANUALENROLMENT = 'manualEnrolment',
-        CSS = {
-            CLOSEBTN : 'close-button',
-            COHORT : 'qce-cohort',
-            COHORTS : 'qce-cohorts',
-            COHORTBUTTON : 'qce-cohort-button',
-            COHORTENROLLED : 'qce-cohort-enrolled',
-            COHORTNAME : 'qce-cohort-name',
-            COHORTUSERS : 'qce-cohort-users',
-            ENROLUSERS : 'canenrolusers',
-            FOOTER : 'qce-footer',
-            HIDDEN : 'hidden',
-            LIGHTBOX : 'qce-loading-lightbox',
-            LOADINGICON : 'loading-icon',
-            MORERESULTS : 'qce-more-results',
-            PANEL : 'qce-panel',
-            PANELCONTENT : 'qce-panel-content',
-            PANELCOHORTS : 'qce-enrollable-cohorts',
-            PANELROLES : 'qce-assignable-roles',
-            PANELCONTROLS : 'qce-panel-controls',
-            SEARCH : 'qce-search'
-        },
-        COUNT = 0;
-
-
-    var CONTROLLER = function(config) {
-        CONTROLLER.superclass.constructor.apply(this, arguments);
-    };
-    CONTROLLER.prototype = {
-        initializer : function(config) {
-            COUNT ++;
-            this.publish('assignablerolesloaded', {fireOnce:true});
-            this.publish('cohortsloaded');
-            this.publish('defaultcohortroleloaded', {fireOnce:true});
-
-            var finishbutton = Y.Node.create('<div class="'+CSS.CLOSEBTN+'"></div>')
-                                   .append(Y.Node.create('<input type="button" value="'+M.str.enrol.finishenrollingusers+'" />'));
-            var base = Y.Node.create('<div class="'+CSS.PANELCONTENT+'"></div>')
-                .append(Y.Node.create('<div class="'+CSS.PANELROLES+'"></div>'))
-                .append(Y.Node.create('<div class="'+CSS.PANELCOHORTS+'"></div>'))
-                .append(Y.Node.create('<div class="'+CSS.FOOTER+'"></div>')
-                    .append(Y.Node.create('<div class="'+CSS.SEARCH+'"><label for="enrolcohortsearch">'+M.str.enrol_cohort.cohortsearch+':</label></div>')
-                        .append(Y.Node.create('<input type="text" id="enrolcohortsearch" value="" />'))
-                    )
-                    .append(finishbutton)
-                )
-                .append(Y.Node.create('<div class="'+CSS.LIGHTBOX+' '+CSS.HIDDEN+'"></div>')
-                    .append(Y.Node.create('<img alt="loading" class="'+CSS.LOADINGICON+'" />')
-                        .setAttribute('src', M.util.image_url('i/loading', 'moodle')))
-                    .setStyle('opacity', 0.5)
-                );
-
-            var close = Y.Node.create('<div class="close"></div>');
-            var panel = new Y.Overlay({
-                headerContent : Y.Node.create('<div></div>').append(Y.Node.create('<h2>'+M.str.enrol.enrolcohort+'</h2>')).append(close),
-                bodyContent : base,
-                constrain : true,
-                centered : true,
-                id : PANELID+COUNT,
-                visible : false
-            });
-
-            // display the wheel on ajax events
-            Y.on('io:start', function() {
-                base.one('.'+CSS.LIGHTBOX).removeClass(CSS.HIDDEN);
-            }, this);
-            Y.on('io:end', function() {
-                base.one('.'+CSS.LIGHTBOX).addClass(CSS.HIDDEN);
-            }, this);
-
-            this.set(SEARCH, base.one('#enrolcohortsearch'));
-            Y.on('key', this.getCohorts, this.get(SEARCH), 'down:13', this, false);
-
-            panel.get('boundingBox').addClass(CSS.PANEL);
-            panel.render(Y.one(document.body));
-            this.on('show', function(){
-                this.set('centered', true);
-                this.show();
-            }, panel);
-            this.on('hide', panel.hide, panel);
-            this.on('assignablerolesloaded', this.updateContent, this, panel);
-            this.on('cohortsloaded', this.updateContent, this, panel);
-            this.on('defaultcohortroleloaded', this.updateContent, this, panel);
-            Y.on('key', this.hide, document.body, 'down:27', this);
-            close.on('click', this.hide, this);
-            finishbutton.on('click', this.hide, this);
-
-            Y.all('.enrol_cohort_plugin input').each(function(node){
-                if (node.getAttribute('type', 'submit')) {
-                    node.on('click', this.show, this);
-                }
-            }, this);
-
-            base = panel.get('boundingBox');
-            base.plug(Y.Plugin.Drag);
-            base.dd.addHandle('.yui3-widget-hd h2');
-            base.one('.yui3-widget-hd h2').setStyle('cursor', 'move');
-        },
-        show : function(e) {
-            e.preventDefault();
-            // prepare the data and display the window
-            this.getCohorts(e, false);
-            this.getAssignableRoles();
-            this.fire('show');
-
-            var rolesselect = Y.one('#id_enrol_cohort_assignable_roles');
-            if (rolesselect) {
-                rolesselect.focus();
-            }
-        },
-        updateContent : function(e, panel) {
-            var content, i, roles, cohorts, count=0, supportmanual = this.get(MANUALENROLMENT), defaultrole;
-            switch (e.type.replace(/^[^:]+:/, '')) {
-                case 'cohortsloaded' :
-                    if (this.get(FIRSTPAGE)) {
-                        // we are on the page 0, create new element for cohorts list
-                        content = Y.Node.create('<div class="'+CSS.COHORTS+'"></div>');
-                        if (supportmanual) {
-                            content.addClass(CSS.ENROLUSERS);
-                        }
-                    } else {
-                        // we are adding cohorts to existing list
-                        content = Y.Node.one('.'+CSS.PANELCOHORTS+' .'+CSS.COHORTS);
-                        content.one('.'+CSS.MORERESULTS).remove();
-                    }
-                    // add cohorts items to the content
-                    cohorts = this.get(COHORTS);
-                    for (i in cohorts) {
-                        count++;
-                        cohorts[i].on('enrolchort', this.enrolCohort, this, cohorts[i], panel.get('contentBox'), false);
-                        cohorts[i].on('enrolusers', this.enrolCohort, this, cohorts[i], panel.get('contentBox'), true);
-                        content.append(cohorts[i].toHTML(supportmanual).addClass((count%2)?'even':'odd'));
-                    }
-                    // add the next link if there are more items expected
-                    if (this.get(MORERESULTS)) {
-                        var fetchmore = Y.Node.create('<div class="'+CSS.MORERESULTS+'"><a href="#">'+M.str.enrol_cohort.ajaxmore+'</a></div>');
-                        fetchmore.on('click', this.getCohorts, this, true);
-                        content.append(fetchmore);
-                    }
-                    // finally assing the content to the block
-                    if (this.get(FIRSTPAGE)) {
-                        panel.get('contentBox').one('.'+CSS.PANELCOHORTS).setContent(content);
-                    }
-                    break;
-                case 'assignablerolesloaded':
-                    roles = this.get(ASSIGNABLEROLES);
-                    content = Y.Node.create('<select id="id_enrol_cohort_assignable_roles"></select>');
-                    for (i in roles) {
-                        content.append(Y.Node.create('<option value="'+i+'">'+roles[i]+'</option>'));
-                    }
-                    panel.get('contentBox').one('.'+CSS.PANELROLES).setContent(Y.Node.create('<div><label for="id_enrol_cohort_assignable_roles">'+M.str.role.assignroles+':</label></div>').append(content));
-
-                    this.getDefaultCohortRole();
-                    Y.one('#id_enrol_cohort_assignable_roles').focus();
-                    break;
-                case 'defaultcohortroleloaded':
-                    defaultrole = this.get(DEFAULTCOHORTROLE);
-                    panel.get('contentBox').one('.'+CSS.PANELROLES+' select').set('value', defaultrole);
-                    break;
-            }
-        },
-        hide : function() {
-            if (this.get(REQUIREREFRESH)) {
-                window.location = this.get(URL);
-            }
-            this.fire('hide');
-        },
-        getCohorts : function(e, append) {
-            if (e) {
-                e.preventDefault();
-            }
-            if (append) {
-                this.set(FIRSTPAGE, false);
-            } else {
-                this.set(FIRSTPAGE, true);
-                this.set(OFFSET, 0);
-            }
-            var params = [];
-            params['id'] = this.get(COURSEID);
-            params['offset'] = this.get(OFFSET);
-            params['search'] = this.get(SEARCH).get('value');
-            params['action'] = 'getcohorts';
-            params['sesskey'] = M.cfg.sesskey;
-
-            Y.io(M.cfg.wwwroot+this.get(AJAXURL), {
-                method:'POST',
-                data:build_querystring(params),
-                on: {
-                    complete: function(tid, outcome, args) {
-                        try {
-                            var cohorts = Y.JSON.parse(outcome.responseText);
-                            if (cohorts.error) {
-                                new M.core.ajaxException(cohorts);
-                            } else {
-                                this.setCohorts(cohorts.response);
-                            }
-                        } catch (e) {
-                            return new M.core.exception(e);
-                        }
-                        this.fire('cohortsloaded');
-                    }
-                },
-                context:this
-            });
-        },
-        setCohorts : function(response) {
-            this.set(MORERESULTS, response.more);
-            this.set(OFFSET, response.offset);
-            var rawcohorts = response.cohorts;
-            var cohorts = [], i=0;
-            for (i in rawcohorts) {
-                cohorts[i] = new COHORT(rawcohorts[i]);
-            }
-            this.set(COHORTS, cohorts);
-        },
-        getAssignableRoles : function() {
-            Y.io(M.cfg.wwwroot+this.get(AJAXURL), {
-                method:'POST',
-                data:'id='+this.get(COURSEID)+'&action=getassignable&sesskey='+M.cfg.sesskey,
-                on: {
-                    complete: function(tid, outcome, args) {
-                        try {
-                            var roles = Y.JSON.parse(outcome.responseText);
-                            this.set(ASSIGNABLEROLES, roles.response);
-                        } catch (e) {
-                            return new M.core.exception(e);
-                        }
-                        this.getAssignableRoles = function() {
-                            this.fire('assignablerolesloaded');
-                        };
-                        this.getAssignableRoles();
-                    }
-                },
-                context:this
-            });
-        },
-        getDefaultCohortRole : function() {
-            Y.io(M.cfg.wwwroot+this.get(AJAXURL), {
-                method:'POST',
-                data:'id='+this.get(COURSEID)+'&action=getdefaultcohortrole&sesskey='+M.cfg.sesskey,
-                on: {
-                    complete: function(tid, outcome, args) {
-                        try {
-                            var roles = Y.JSON.parse(outcome.responseText);
-                            this.set(DEFAULTCOHORTROLE, roles.response);
-                        } catch (e) {
-                            return new M.core.exception(e);
-                        }
-                        this.fire('defaultcohortroleloaded');
-                    }
-                },
-                context:this
-            });
-        },
-        enrolCohort : function(e, cohort, node, usersonly) {
-            var params = {
-                id : this.get(COURSEID),
-                roleid : node.one('.'+CSS.PANELROLES+' select').get('value'),
-                cohortid : cohort.get(COHORTID),
-                action : (usersonly)?'enrolcohortusers':'enrolcohort',
-                sesskey : M.cfg.sesskey
-            };
-            Y.io(M.cfg.wwwroot+this.get(AJAXURL), {
-                method:'POST',
-                data:build_querystring(params),
-                on: {
-                    complete: function(tid, outcome, args) {
-                        try {
-                            var result = Y.JSON.parse(outcome.responseText);
-                            if (result.error) {
-                                new M.core.ajaxException(result);
-                            } else {
-                                if (result.response && result.response.message) {
-                                    var alertpanel = new M.core.alert(result.response);
-                                    Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
-                                }
-                                var enrolled = Y.Node.create('<div class="'+CSS.COHORTBUTTON+' alreadyenrolled">'+M.str.enrol.synced+'</div>');
-                                node.one('.'+CSS.COHORT+' #cohortid_'+cohort.get(COHORTID)).replace(enrolled);
-                                this.set(REQUIREREFRESH, true);
-                            }
-                        } catch (e) {
-                            new M.core.exception(e);
-                        }
-                    }
-                },
-                context:this
-            });
-            return true;
-        }
-    };
-    Y.extend(CONTROLLER, Y.Base, CONTROLLER.prototype, {
-        NAME : CONTROLLERNAME,
-        ATTRS : {
-            url : {
-                validator : Y.Lang.isString
-            },
-            ajaxurl : {
-                validator : Y.Lang.isString
-            },
-            courseid : {
-                value : null
-            },
-            cohorts : {
-                validator : Y.Lang.isArray,
-                value : null
-            },
-            assignableRoles : {
-                value : null
-            },
-            manualEnrolment : {
-                value : false
-            },
-            defaultCohortRole : {
-                value : null
-            },
-            requiresRefresh : {
-                value : false,
-                validator : Y.Lang.isBool
-            }
-        }
-    });
-    Y.augment(CONTROLLER, Y.EventTarget);
-
-    var COHORT = function(config) {
-        COHORT.superclass.constructor.apply(this, arguments);
-    };
-    Y.extend(COHORT, Y.Base, {
-        toHTML : function(supportmanualenrolment){
-            var button, result, name, users, syncbutton, usersbutton;
-            result = Y.Node.create('<div class="'+CSS.COHORT+'"></div>');
-            if (this.get(ENROLLED)) {
-                button = Y.Node.create('<div class="'+CSS.COHORTBUTTON+' alreadyenrolled">'+M.str.enrol.synced+'</div>');
-            } else {
-                button = Y.Node.create('<div id="cohortid_'+this.get(COHORTID)+'"></div>');
-
-                syncbutton = Y.Node.create('<a class="'+CSS.COHORTBUTTON+' notenrolled enrolcohort">'+M.str.enrol.enrolcohort+'</a>');
-                syncbutton.on('click', function(){this.fire('enrolchort');}, this);
-                button.append(syncbutton);
-
-                if (supportmanualenrolment) {
-                    usersbutton = Y.Node.create('<a class="'+CSS.COHORTBUTTON+' notenrolled enrolusers">'+M.str.enrol.enrolcohortusers+'</a>');
-                    usersbutton.on('click', function(){this.fire('enrolusers');}, this);
-                    button.append(usersbutton);
-                }
-            }
-            name = Y.Node.create('<div class="'+CSS.COHORTNAME+'">'+this.get(NAME)+'</div>');
-            users = Y.Node.create('<div class="'+CSS.COHORTUSERS+'">'+this.get(USERS)+'</div>');
-            return result.append(button).append(name).append(users);
-        }
-    }, {
-        NAME : COHORTNAME,
-        ATTRS : {
-            cohortid : {
-
-            },
-            name : {
-                validator : Y.Lang.isString
-            },
-            enrolled : {
-                value : false
-            },
-            users : {
-                value : 0
-            }
-        }
-    });
-    Y.augment(COHORT, Y.EventTarget);
-
-    M.enrol_cohort = M.enrol || {};
-    M.enrol_cohort.quickenrolment = {
-        init : function(cfg) {
-            new CONTROLLER(cfg);
-        }
-    }
-
-}, '@VERSION@', {requires:['base','node', 'overlay', 'io-base', 'test', 'json-parse', 'event-delegate', 'dd-plugin', 'event-key', 'moodle-core-notification']});
index c1a6f80..86bc7a0 100644 (file)
@@ -30,6 +30,8 @@ define('AJAX_SCRIPT', true);
 require('../../config.php');
 require_once($CFG->dirroot.'/enrol/locallib.php');
 require_once($CFG->dirroot.'/group/lib.php');
+require_once($CFG->dirroot.'/enrol/manual/locallib.php');
+require_once($CFG->dirroot.'/cohort/lib.php');
 
 $id      = required_param('id', PARAM_INT); // Course id.
 $action  = required_param('action', PARAM_ALPHANUMEXT);
@@ -94,9 +96,28 @@ switch ($action) {
         $outcome->response['users'] = array_values($outcome->response['users']);
         $outcome->success = true;
         break;
+    case 'searchcohorts':
+        $enrolid = required_param('enrolid', PARAM_INT);
+        $search = optional_param('search', '', PARAM_RAW);
+        $page = optional_param('page', 0, PARAM_INT);
+        $addedenrollment = optional_param('enrolcount', 0, PARAM_INT);
+        $perpage = optional_param('perpage', 25, PARAM_INT);  //  This value is hard-coded to 25 in quickenrolment.js
+        $outcome->response = enrol_manual_get_potential_cohorts($context, $enrolid, $search, $page, $perpage, $addedenrollment);
+        $outcome->success = true;
+        break;
     case 'enrol':
         $enrolid = required_param('enrolid', PARAM_INT);
-        $userid = required_param('userid', PARAM_INT);
+        $cohort = $user = null;
+        $cohortid = optional_param('cohortid', 0, PARAM_INT);
+        if (!$cohortid) {
+            $userid = required_param('userid', PARAM_INT);
+            $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
+        } else {
+            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+            if (!cohort_can_view_cohort($cohort, $context)) {
+                throw new enrol_ajax_exception('invalidenrolinstance'); // TODO error text!
+            }
+        }
 
         $roleid = optional_param('role', null, PARAM_INT);
         $duration = optional_param('duration', 0, PARAM_INT);
@@ -124,7 +145,6 @@ switch ($action) {
             $timeend = $timestart + ($duration*24*60*60);
         }
 
-        $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
         $instances = $manager->get_enrolment_instances();
         $plugins = $manager->get_enrolment_plugins(true); // Do not allow actions on disabled plugins.
         if (!array_key_exists($enrolid, $instances)) {
@@ -136,7 +156,11 @@ switch ($action) {
         }
         $plugin = $plugins[$instance->enrol];
         if ($plugin->allow_enrol($instance) && has_capability('enrol/'.$plugin->get_name().':enrol', $context)) {
-            $plugin->enrol_user($instance, $user->id, $roleid, $timestart, $timeend, null, $recovergrades);
+            if ($user) {
+                $plugin->enrol_user($instance, $user->id, $roleid, $timestart, $timeend, null, $recovergrades);
+            } else {
+                $plugin->enrol_cohort($instance, $cohort->id, $roleid, $timestart, $timeend, null, $recovergrades);
+            }
         } else {
             throw new enrol_ajax_exception('enrolnotpermitted');
         }
index 74c915a..6c29c66 100644 (file)
@@ -26,6 +26,8 @@ $string['alterstatus'] = 'Alter status';
 $string['altertimeend'] = 'Alter end time';
 $string['altertimestart'] = 'Alter start time';
 $string['assignrole'] = 'Assign role';
+$string['browseusers'] = 'Browse users';
+$string['browsecohorts'] = 'Browse cohorts';
 $string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
 $string['defaultperiod'] = 'Default enrolment duration';
 $string['defaultperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.';
index 5ccd1a3..34d4d30 100644 (file)
@@ -202,6 +202,7 @@ class enrol_manual_plugin extends enrol_plugin {
      */
     public function get_manual_enrol_button(course_enrolment_manager $manager) {
         global $CFG;
+        require_once($CFG->dirroot.'/cohort/lib.php');
 
         $instance = null;
         $instances = array();
@@ -245,7 +246,8 @@ class enrol_manual_plugin extends enrol_plugin {
             'defaultRole'         => $instance->roleid,
             'defaultDuration'     => $defaultduration,
             'disableGradeHistory' => $CFG->disablegradehistory,
-            'recoverGradesDefault'=> ''
+            'recoverGradesDefault'=> '',
+            'cohortsAvailable'    => cohort_get_available_cohorts($manager->get_context(), COHORT_COUNT_MEMBERS, 0, 1) ? true : false
         );
 
         if ($CFG->recovergradesdefault) {
@@ -271,6 +273,7 @@ class enrol_manual_plugin extends enrol_plugin {
             'enrolperiod',
             'finishenrollingusers',
             'recovergrades'), 'enrol');
+        $button->strings_for_js(array('browseusers', 'browsecohorts'), 'enrol_manual');
         $button->strings_for_js('assignroles', 'role');
         $button->strings_for_js('startingfrom', 'moodle');
 
@@ -575,4 +578,28 @@ class enrol_manual_plugin extends enrol_plugin {
         $context = context_course::instance($instance->courseid);
         return has_capability('enrol/manual:config', $context);
     }
+
+    /**
+     * Enrol all not enrolled cohort members into course via enrol instance.
+     *
+     * @param stdClass $instance
+     * @param int $cohortid
+     * @param int $roleid optional role id
+     * @param int $timestart 0 means unknown
+     * @param int $timeend 0 means forever
+     * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
+     * @param bool $recovergrades restore grade history
+     */
+    public function enrol_cohort(stdClass $instance, $cohortid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
+        global $DB;
+        $context = context_course::instance($instance->courseid);
+        list($esql, $params) = get_enrolled_sql($context);
+        $sql = "SELECT cm.userid FROM {cohort_members} cm LEFT JOIN ($esql) u ON u.id = cm.userid ".
+            "WHERE cm.cohortid = :cohortid AND u.id IS NULL";
+        $params['cohortid'] = $cohortid;
+        $members = $DB->get_fieldset_sql($sql, $params);
+        foreach ($members as $userid) {
+            $this->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status, $recovergrades);
+        }
+    }
 }
index a4f7a00..472897f 100644 (file)
@@ -464,3 +464,34 @@ function enrol_manual_migrate_plugin_enrolments($enrol) {
     }
     $rs->close();
 }
+
+/**
+ * Gets an array of the cohorts that can be enrolled in this course.
+ *
+ * @param int $enrolid
+ * @param string $search
+ * @param int $page Defaults to 0
+ * @param int $perpage Defaults to 25
+ * @param int $addedenrollment
+ * @return array Array(totalcohorts => int, cohorts => array)
+ */
+function enrol_manual_get_potential_cohorts($context, $enrolid, $search = '', $page = 0, $perpage = 25, $addedenrollment = 0) {
+    global $CFG;
+    require_once($CFG->dirroot . '/cohort/lib.php');
+
+    $allcohorts = cohort_get_available_cohorts($context, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY, 0, 0, $search);
+    $totalcohorts = count($allcohorts);
+    $cohorts = array();
+    $cnt = 0;
+    foreach ($allcohorts as $c) {
+        if ($cnt >= $page * $perpage && (!$perpage || $cnt < ($page+1)*$perpage)) {
+            $cohorts[] = (object)array(
+                'id' => $c->id,
+                'name' => format_string($c->name, true, array('context' => $c->contextid)),
+                'cnt' => $c->memberscnt - $c->enrolledcnt
+            );
+        }
+        $cnt++;
+    }
+    return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts);
+}
index e8a4cda..d1ae180 100644 (file)
@@ -44,6 +44,7 @@ Structure of the user enroller panel
 
 .user-enroller-panel .uep-content {text-align:center;position:relative;width:100%;border-top:1px solid #999;border-bottom:1px solid #999;}
 .user-enroller-panel .uep-content .uep-controls {margin:0;padding:3px;background-color:#ddd;text-align:left;border-bottom:1px solid #BBB;}
+.user-enroller-panel .uep-content .uep-controls label {display: inline;padding-right:5px;}
 .user-enroller-panel .uep-content .uep-controls .uep-enrolment-option input {vertical-align:middle;margin-left:1em;}
 .user-enroller-panel .uep-ajax-content {height:375px;overflow:auto;}
 .user-enroller-panel .uep-search-results .totalusers {background-color:#eee;padding:5px;border-bottom:1px solid #BBB;font-size:7pt;font-weight: bold;}
@@ -57,6 +58,14 @@ Structure of the user enroller panel
 .user-enroller-panel .uep-search-results .user.enrolled .count {width:40px;color:#eee;}
 .user-enroller-panel .uep-search-results .uep-more-results {background-color:#eee;padding:5px;border-top:1px solid #BBB;}
 
+.user-enroller-panel .uep-search-results .totalcohorts {background-color:#eee;padding:5px;border-bottom:1px solid #BBB;font-size:7pt;font-weight: bold;}
+.user-enroller-panel .uep-search-results .cohort {width:100%;text-align:left;font-size:9pt;border-bottom:1px solid #ddd;border-top:1px solid #eee;}
+.user-enroller-panel .uep-search-results .cohort .count {width:20px;float:left;font-size:7pt;line-height:27px;border-right:1px solid #ddd;background-color:#EEE;text-align:right;padding:2px;}
+.user-enroller-panel .uep-search-results .cohort .details {width:250px;float:left;margin:5px;}
+.user-enroller-panel .uep-search-results .cohort .options {padding-right:7px;font-size:8pt;margin:3px;}
+.user-enroller-panel .uep-search-results .cohort .options .enrol {margin:3px;float:right;cursor:pointer;}
+.user-enroller-panel .uep-search-results .cohort.enrolled .count {width:40px;color:#eee;}
+
 .user-enroller-panel .uep-loading-lightbox {position:absolute;width:100%;height:100%;top:0;left:0;background-color:#FFF;min-width:50px;min-height:50px;}
 .user-enroller-panel .uep-loading-lightbox.hidden {display:none;}
 .user-enroller-panel .uep-loading-lightbox .loading-icon {margin:auto;vertical-align:middle;margin-top:125px;}
@@ -67,7 +76,7 @@ Structure of the user enroller panel
 .user-enroller-panel .uep-search input {width:50%;}
 .user-enroller-panel .uep-search input.uep-search-btn {width:20%;}
 .user-enroller-panel .uep-searchoptions {margin:3px;cursor:pointer;}
-.user-enroller-panel .uep-searchoptions select {margin-left:1em;}
+.user-enroller-panel .uep-controls select {margin-left:1em;margin-bottom:0;}
 .user-enroller-panel .collapsibleheading img {margin-right:8px;}
 .user-enroller-panel .collapsiblearea {border:1px solid #bbb;background-color:#f6f6f6;}
 .user-enroller-panel .collapsiblearea.hidden {display:none;}
@@ -75,4 +84,5 @@ Structure of the user enroller panel
 
 .dir-rtl .user-enroller-panel .uep-header .close {right: auto;left:1em;}
 .dir-rtl .user-enroller-panel .uep-search-results .user { text-align: right;}
-.dir-rtl .user-enroller-panel .uep-content .uep-controls { text-align: right;}
\ No newline at end of file
+.dir-rtl .user-enroller-panel .uep-content .uep-controls { text-align: right;}
+.dir-rtl .user-enroller-panel .uep-search-results .cohort { text-align: right;}
index dee47d8..46e6e1f 100644 (file)
@@ -25,7 +25,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
         DISABLEGRADEHISTORY : 'disableGradeHistory',
         RECOVERGRADESDEFAULT : 'recoverGradesDefault',
         ENROLCOUNT : 'enrolCount',
-        PERPAGE : 'perPage'
+        PERPAGE : 'perPage',
+        COHORTSAVAILABLE : 'cohortsAvailable',
+        COHORTCOUNT : 'cohortCount'
     };
     /** CSS classes for nodes in structure **/
     var CSS = {
@@ -67,7 +69,12 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
         SEARCH : 'uep-search',
         SEARCHBTN : 'uep-search-btn',
         CLOSE : 'close',
-        CLOSEBTN : 'close-button'
+        CLOSEBTN : 'close-button',
+        ENTITYSELECTOR : 'uep-entity-selector',
+        COHORTS : 'cohorts',
+        COHORT : 'cohort',
+        COHORTNAME : 'cohortname',
+        TOTALCOHORTS : 'totalcohorts'
     };
     var create = Y.Node.create;
 
@@ -96,6 +103,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                             .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.ROLE+'"><label for="id_enrol_manual_assignable_roles">'+M.str.role.assignroles+'</label></div>')
                                     .append(create('<select id="id_enrol_manual_assignable_roles"><option value="">'+M.str.enrol.none+'</option></select>'))
                             )
+                            .append(create('<div class="'+CSS.ENTITYSELECTOR+'"></div>'))
                             .append(create('<div class="'+CSS.SEARCHOPTIONS+'"></div>')
                                 .append(create('<div class="'+CSS.COLLAPSIBLEHEADING+'"><img alt="" />'+M.str.enrol.enrolmentoptions+'</div>'))
                                 .append(create('<div class="'+CSS.COLLAPSIBLEAREA+' '+CSS.HIDDEN+'"></div>')
@@ -141,6 +149,19 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             Y.on('key', this.preSearch, this.get(UEP.SEARCH), 'down:13', this);
             this.get(UEP.SEARCHBTN).on('click', this.preSearch, this);
 
+            if (this.get(UEP.COHORTSAVAILABLE)) {
+                this.get(UEP.BASE).one('.'+CSS.ENTITYSELECTOR)
+                    .append(create('<input type="radio" id="id_enrol_manual_entity_users" name="enrol_manual_entity" value="users" checked="checked"/>'))
+                    .append(create('<label for="id_enrol_manual_entity_users">'+ M.str.enrol_manual.browseusers+'</label>'))
+                    .append(create('<input type="radio" id="id_enrol_manual_entity_cohorts" name="enrol_manual_entity" value="cohorts"/>'))
+                    .append(create('<label for="id_enrol_manual_entity_cohorts">'+M.str.enrol_manual.browsecohorts+'</label>'));
+                this.get(UEP.BASE).one('#id_enrol_manual_entity_cohorts').on('change', this.search, this);
+                this.get(UEP.BASE).one('#id_enrol_manual_entity_users').on('change', this.search, this);
+            } else {
+                this.get(UEP.BASE).one('.'+CSS.ENTITYSELECTOR)
+                    .append(create('<input type="hidden" name="enrol_manual_entity" value="users"/>'));
+            }
+
             Y.one(document.body).append(this.get(UEP.BASE));
 
             var base = this.get(UEP.BASE);
@@ -291,7 +312,17 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 window.location = this.get(UEP.URL);
             }
         },
+        currentEntity : function() {
+            var entity = CSS.USER;
+            var cohortsinput = Y.one('#id_enrol_manual_entity_cohorts');
+            if (cohortsinput && cohortsinput.get('checked')) {
+                entity = CSS.COHORT;
+            }
+            return entity;
+        },
         search : function(e, append) {
+            var entity = this.currentEntity();
+
             if (e) {
                 e.halt();
                 e.preventDefault();
@@ -301,11 +332,12 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 this.set(UEP.PAGE, this.get(UEP.PAGE)+1);
             } else {
                 this.set(UEP.USERCOUNT, 0);
+                this.set(UEP.COHORTCOUNT, 0);
                 this.set(UEP.PAGE, 0);
             }
             params = this.get(UEP.PARAMS);
             params['sesskey'] = M.cfg.sesskey;
-            params['action'] = 'searchusers';
+            params['action'] = (entity === CSS.USER) ? 'searchusers' : 'searchcohorts';
             params['search'] = this.get(UEP.SEARCH).get('value');
             params['page'] = this.get(UEP.PAGE);
             params['enrolcount'] = this.get(UEP.ENROLCOUNT);
@@ -322,7 +354,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 data:build_querystring(params),
                 on : {
                     start : this.displayLoading,
-                    complete: this.processSearchResults,
+                    complete: ((entity === CSS.USER) ? this.processSearchResults : this.processCohortsSearchResults),
                     end : this.removeLoading
                 },
                 context:this,
@@ -391,11 +423,68 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 }
             }
         },
+        processCohortsSearchResults : function(tid, outcome, args) {
+            try {
+                var result = Y.JSON.parse(outcome.responseText);
+                if (result.error) {
+                    return new M.core.ajaxException(result);
+                }
+            } catch (e) {
+                new M.core.exception(e);
+            }
+            if (!result.success) {
+                this.setContent = M.str.enrol.errajaxsearch;
+            }
+            var cohorts;
+            if (!args.append) {
+                cohorts = create('<div class="'+CSS.COHORTS+'"></div>');
+            } else {
+                cohorts = this.get(UEP.BASE).one('.'+CSS.SEARCHRESULTS+' .'+CSS.COHORTS);
+            }
+            var count = this.get(UEP.COHORTCOUNT);
+            for (var i in result.response.cohorts) {
+                count++;
+                var cohort = result.response.cohorts[i];
+                cohorts.append(create('<div class="'+CSS.COHORT+' clearfix" rel="'+cohort.id+'"></div>')
+                    .addClass((count%2)?CSS.ODD:CSS.EVEN)
+                    .append(create('<div class="'+CSS.COUNT+'">'+count+'</div>'))
+                    .append(create('<div class="'+CSS.DETAILS+'"></div>')
+                        .append(create('<div class="'+CSS.COHORTNAME+'">'+cohort.name+'</div>')))
+                    .append(create('<div class="'+CSS.OPTIONS+'"></div>')
+                        .append(create('<input type="button" class="'+CSS.ENROL+'" value="'+'Enrol '+cohort.cnt+' users'+'" />'))) // TODO string
+                );
+            }
+            this.set(UEP.COHORTCOUNT, count);
+            if (!args.append) {
+                //var usersstr = (result.response.totalusers == '1')?M.str.enrol.ajaxoneuserfound:M.util.get_string('ajaxxusersfound','enrol', result.response.totalusers);
+                var cohortsstr = 'Found '+result.response.totalcohorts+' cohorts'; // TODO
+                var content = create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
+                    .append(create('<div class="'+CSS.TOTALCOHORTS+'">'+cohortsstr+'</div>'))
+                    .append(cohorts);
+                if (result.response.totalcohorts > (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
+                    var fetchmore = create('<div class="'+CSS.MORERESULTS+'"><a href="#">'+M.str.enrol.ajaxnext25+'</a></div>');
+                    fetchmore.on('click', this.search, this, true);
+                    content.append(fetchmore)
+                }
+                this.setContent(content);
+                Y.delegate("click", this.enrolUser, cohorts, '.'+CSS.COHORT+' .'+CSS.ENROL, this, args);
+            } else {
+                if (result.response.totalcohorts <= (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
+                    this.get(UEP.BASE).one('.'+CSS.MORERESULTS).remove();
+                }
+            }
+        },
         enrolUser : function(e, args) {
-            var user = e.currentTarget.ancestor('.'+CSS.USER);
+            var entityname = this.currentEntity();
+
+            var entity = e.currentTarget.ancestor('.'+entityname);
             var params = [];
             params['id'] = this.get(UEP.COURSEID);
-            params['userid'] = user.getAttribute("rel");
+            if (entityname === CSS.USER) {
+                params['userid'] = entity.getAttribute("rel");
+            } else {
+                params['cohortid'] = entity.getAttribute("rel");
+            }
             params['enrolid'] = args.enrolid;
             params['sesskey'] = M.cfg.sesskey;
             params['action'] = 'enrol';
@@ -419,8 +508,8 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                             if (result.error) {
                                 return new M.core.ajaxException(result);
                             } else {
-                                args.userNode.addClass(CSS.ENROLLED);
-                                args.userNode.one('.'+CSS.ENROL).remove();
+                                args.entityNode.addClass(CSS.ENROLLED);
+                                args.entityNode.one('.'+CSS.ENROL).remove();
                                 this.set(UEP.REQUIREREFRESH, true);
                                 var countenrol = this.get(UEP.ENROLCOUNT)+1;
                                 this.set(UEP.ENROLCOUNT, countenrol);
@@ -434,7 +523,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 context:this,
                 arguments:{
                     params : params,
-                    userNode : user
+                    entityNode : entity
                 }
             });
 
@@ -544,6 +633,13 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             perPage : {
                 value: 25,
                 Validator: Y.Lang.isNumber
+            },
+            cohortCount : {
+                value : 0,
+                validator : Y.Lang.isNumber
+            },
+            cohortsAvailable : {
+                value : null
             }
         }
     });