Merge branch 'MDL-54027-master' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Wed, 18 May 2016 10:05:06 +0000 (11:05 +0100)
committerDan Poltawski <dan@moodle.com>
Wed, 18 May 2016 10:05:06 +0000 (11:05 +0100)
254 files changed:
admin/environment.xml
admin/tool/cohortroles/lang/en/tool_cohortroles.php
admin/tool/lp/amd/build/dragdrop-reorder.min.js
admin/tool/lp/amd/src/dragdrop-reorder.js
admin/tool/lp/classes/external/course_summary_exporter.php
admin/tool/lp/classes/form/competency.php
admin/tool/lp/classes/form/competency_framework.php
admin/tool/lp/lang/en/tool_lp.php
admin/tool/lp/styles.css
admin/tool/lp/templates/competencies_move_tree.mustache
admin/tool/lp/templates/competencies_tree.mustache
admin/tool/lp/templates/competencies_tree_root.mustache
admin/tool/lp/templates/competency_path.mustache
admin/tool/lp/templates/competency_picker.mustache
admin/tool/lp/templates/competency_picker_competencyform.mustache
admin/tool/lp/templates/competency_picker_user_plans.mustache
admin/tool/lp/templates/competency_plan_navigation.mustache
admin/tool/lp/templates/competency_rule_points.mustache
admin/tool/lp/templates/competency_summary.mustache
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/course_competency_statistics.mustache
admin/tool/lp/templates/manage_competencies_page.mustache
admin/tool/lp/templates/manage_competency_frameworks_page.mustache
admin/tool/lp/templates/manage_templates_page.mustache
admin/tool/lp/templates/no_frameworks_warning.mustache
admin/tool/lp/templates/plan_page.mustache
admin/tool/lp/templates/plans_page.mustache
admin/tool/lp/templates/progress_bar.mustache [new file with mode: 0644]
admin/tool/lp/templates/related_competencies.mustache
admin/tool/lp/templates/template_competencies_page.mustache
admin/tool/lp/templates/template_statistics.mustache
admin/tool/lp/templates/user_competency_course_navigation.mustache
admin/tool/lp/templates/user_evidence_list_page.mustache
admin/tool/lp/templates/user_evidence_page.mustache
admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-debug.js
admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js
admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder.js
admin/tool/lp/yui/src/dragdrop-reorder/js/dragdropreorder.js
admin/tool/lpmigrate/templates/migrate_frameworks_results.mustache
admin/tool/recyclebin/classes/category_bin.php
admin/tool/recyclebin/classes/course_bin.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/helper_test.php
admin/tool/uploadcourse/tests/processor_test.php
auth/ldap/auth.php
auth/lti/lang/en/auth_lti.php
backup/controller/backup_controller.class.php
backup/controller/restore_controller.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_course_format_test.php
backup/moodle2/tests/moodle2_test.php
backup/upgrade.txt
backup/util/checks/tests/checks_test.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_helper.class.php
backup/util/loggers/base_logger.class.php
backup/util/loggers/file_logger.class.php
backup/util/loggers/tests/logger_test.php
backup/util/plan/tests/plan_test.php
backup/util/plan/tests/step_test.php
backup/util/plan/tests/task_test.php
blocks/completionstatus/tests/behat/block_completionstatus.feature [new file with mode: 0644]
blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature [new file with mode: 0644]
blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature [new file with mode: 0644]
blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature [new file with mode: 0644]
blocks/course_list/tests/behat/block_course_list_category.feature [new file with mode: 0644]
blocks/course_list/tests/behat/block_course_list_course.feature [new file with mode: 0644]
blocks/course_list/tests/behat/block_course_list_dashboard.feature [new file with mode: 0644]
blocks/course_list/tests/behat/block_course_list_frontpage.feature [new file with mode: 0644]
blocks/course_overview/tests/behat/block_course_overview.feature [new file with mode: 0644]
blocks/lp/templates/competencies_to_review_page.mustache
blocks/lp/templates/plans_to_review_page.mustache
blocks/lp/templates/summary.mustache
blocks/navigation/amd/build/ajax_response_renderer.min.js
blocks/navigation/amd/src/ajax_response_renderer.js
blocks/navigation/renderer.php
blocks/navigation/styles.css
blocks/rss_client/edit_form.php
blocks/settings/renderer.php
blocks/settings/styles.css
blocks/site_main_menu/block_site_main_menu.php
blocks/site_main_menu/styles.css
calendar/externallib.php
calendar/lib.php
calendar/tests/externallib_test.php
competency/classes/api.php
competency/classes/competency.php
competency/classes/competency_framework.php
competency/classes/external/user_summary_exporter.php
competency/classes/persistent.php
competency/classes/plan_competency.php
competency/classes/template_competency.php
competency/classes/user_competency.php
competency/classes/user_competency_course.php
competency/classes/user_competency_plan.php
competency/tests/api_test.php
competency/tests/external_test.php
competency/tests/persistent_test.php
course/classes/search/mycourse.php [new file with mode: 0644]
course/lib.php
course/tests/courselib_test.php
course/tests/externallib_test.php
course/tests/restore_test.php
course/tests/search_test.php [new file with mode: 0644]
enrol/externallib.php
enrol/guest/classes/external.php
enrol/lti/lang/en/enrol_lti.php
enrol/lti/settings.php
enrol/lti/tests/behat/basic_settings.feature
enrol/lti/tests/behat/index_page.feature
enrol/lti/tool.php
enrol/self/externallib.php
grade/grading/form/guide/db/upgrade.php [new file with mode: 0644]
grade/grading/form/guide/lib.php
grade/grading/form/guide/renderer.php
grade/grading/form/guide/version.php
grade/tests/behat/grade_letter_boundary.feature [new file with mode: 0644]
grade/tests/behat/grade_letter_boundary_20160518.feature [new file with mode: 0644]
install/lang/pt_br/admin.php
lang/en/admin.php
lang/en/auth.php
lang/en/moodle.php
lang/en/search.php
lib/amd/build/str.min.js
lib/amd/build/tree.min.js
lib/amd/src/str.js
lib/amd/src/tree.js
lib/classes/date.php
lib/classes/output/notification.php
lib/classes/user.php
lib/datalib.php
lib/db/install.php
lib/db/services.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/filestorage/file_storage.php
lib/formslib.php
lib/grade/grade_scale.php
lib/gradelib.php
lib/ldaplib.php
lib/moodlelib.php
lib/outputrenderers.php
lib/testing/classes/util.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_navigation.php
lib/tests/date_test.php
lib/tests/moodlelib_test.php
lib/tests/questionlib_test.php
lib/tests/upgradelib_test.php
lib/tests/user_test.php
lib/upgradelib.php
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/dragdrop/js/dragdrop.js
lib/yui/src/lockscroll/js/lockscroll.js
lib/yui/src/notification/js/dialogue.js
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/pix/drag.png [new file with mode: 0644]
mod/assign/feedback/editpdf/pix/drag.svg [new file with mode: 0644]
mod/assign/feedback/editpdf/settings.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/testunoconv.php [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/edit.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js
mod/assign/feedback/offline/importgradesform.php
mod/assign/feedback/offline/importgradeslib.php
mod/assign/feedback/offline/locallib.php
mod/assign/feedback/offline/uploadgradesform.php
mod/assign/gradeform.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/assign/tests/behat/quickgrading.feature
mod/book/lang/en/book.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/data/lib.php
mod/feedback/db/install.xml
mod/feedback/db/upgrade.php
mod/feedback/db/upgradelib.php
mod/feedback/lang/en/feedback.php
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/tests/behat/export_import.feature
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature
mod/feedback/tests/upgradelib_test.php
mod/feedback/version.php
mod/folder/lang/en/folder.php
mod/forum/lang/en/forum.php
mod/glossary/lang/en/glossary.php
mod/imscp/lang/en/imscp.php
mod/label/lang/en/label.php
mod/lesson/lang/en/lesson.php
mod/lti/classes/external.php
mod/lti/edit_form.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/styles.css
mod/lti/tests/behat/addtool.feature
mod/lti/tests/behat/addtype.feature
mod/lti/tests/behat/toolconfigure.feature
mod/lti/tests/service_exception_handler_test.php
mod/page/lang/en/page.php
mod/quiz/classes/external.php
mod/quiz/lang/en/quiz.php
mod/quiz/tests/behat/manually_mark_question.feature
mod/quiz/tests/external_test.php
mod/resource/lang/en/resource.php
mod/scorm/lang/en/scorm.php
mod/survey/lang/en/survey.php
mod/url/lang/en/url.php
mod/wiki/lang/en/wiki.php
mod/workshop/lang/en/workshop.php
pix/y/loading.gif
question/classes/external.php
question/type/multichoice/tests/behat/export.feature
rating/lib.php
rating/tests/rating_test.php
report/competency/templates/report.mustache
repository/filepicker.php
repository/filesystem/lib.php
repository/lib.php
repository/repository_ajax.php
search/classes/area/base.php
search/engine/solr/lang/en/search_solr.php
search/tests/manager_test.php
tag/classes/external.php
tag/tests/events_test.php
theme/base/style/user.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/search.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
user/tests/userlib_test.php
version.php
webservice/upgrade.txt
webservice/xmlrpc/lib.php
webservice/xmlrpc/locallib.php

index 1fc8ce1..f281b45 100644 (file)
           <ON_CHECK message="unsupporteddbtablerowformat" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
 </COMPATIBILITY_MATRIX>
index e735f7e..08f00c9 100644 (file)
 
 $string['acohortroleassignmentssaved'] = '{$a} cohort role assignments were saved.';
 $string['assign'] = 'Assign';
-$string['assignroletocohort'] = 'Assign role to users in the context of all members of these cohorts';
-$string['backgroundsync'] = 'Note: Roles assigned on this page will not take affect immediately, the changes to role assignments will be made by a background task.';
+$string['assignroletocohort'] = 'Assign user-context roles to all cohort members';
+$string['backgroundsync'] = 'Note: New cohort role assignments will not take effect immediately. Role assignment changes will be made by a scheduled task.';
 $string['cohortroleassignmentremoved'] = 'The cohort role assignment was removed.';
 $string['cohortroleassignmentnotremoved'] = 'The cohort role assignment was not removed.';
 $string['cohortroles'] = 'Cohort roles';
 $string['existingcohortroles'] = 'Existing cohort role assignments';
-$string['managecohortroles'] = 'Assign user roles for entire cohort';
-$string['noassignableroles'] = 'No roles can be assigned at a user context level. <a href="../../roles/manage.php">Manage roles</a>';
+$string['managecohortroles'] = 'Assign user roles to cohort';
+$string['noassignableroles'] = 'There are currently no roles that can be assigned in the user context. <a href="../../roles/manage.php">Manage roles</a>';
 $string['nocohortroleassignmentssaved'] = 'No cohort role assignments were saved.';
 $string['onecohortroleassignmentsaved'] = 'One cohort role assignment was saved.';
 $string['pluginname'] = 'Cohort roles management';
index 77c33ba..48751b5 100644 (file)
Binary files a/admin/tool/lp/amd/build/dragdrop-reorder.min.js and b/admin/tool/lp/amd/build/dragdrop-reorder.min.js differ
index 433604d..d14c9dd 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define(['core/str', 'core/yui'], function(str, Y) {
-
     // Private variables and functions.
 
+    /**
+     * Store the current instance of the core drag drop.
+     *
+     * @property dragDropInstance M.tool_lp.dragdrop_reorder
+     */
+    var dragDropInstance = null;
+
     /**
      * Translate the drophit event from YUI
      * into simple drag and drop nodes.
@@ -70,7 +76,10 @@ define(['core/str', 'core/yui'], function(str, Y) {
                     var context = {
                         callback: callback
                     };
-                    M.tool_lp.dragdrop_reorder({
+                    if (dragDropInstance) {
+                        dragDropInstance.destroy();
+                    }
+                    dragDropInstance = M.tool_lp.dragdrop_reorder({
                         group: group,
                         dragHandleText: dragHandleText,
                         sameNodeText: sameNodeText,
index 4f44a7d..ace8117 100644 (file)
@@ -58,7 +58,7 @@ class course_summary_exporter extends \core_competency\external\exporter {
                 'type' => PARAM_TEXT,
             ),
             'idnumber' => array(
-                'type' => PARAM_TEXT,
+                'type' => PARAM_RAW,
             )
         );
     }
index 915ea6f..32136bc 100644 (file)
@@ -102,7 +102,7 @@ class competency extends persistent {
         $mform->setType('description', PARAM_RAW);
         // ID number.
         $mform->addElement('text', 'idnumber', get_string('idnumber', 'tool_lp'), 'maxlength="100"');
-        $mform->setType('idnumber', PARAM_TEXT);
+        $mform->setType('idnumber', PARAM_RAW);
         $mform->addRule('idnumber', null, 'required', null, 'client');
         $mform->addRule('idnumber', get_string('maximumchars', '', 100), 'maxlength', 100, 'client');
 
index 6dde66c..6d4b6bc 100644 (file)
@@ -65,7 +65,7 @@ class competency_framework extends persistent {
         $mform->setType('description', PARAM_RAW);
         // ID number.
         $mform->addElement('text', 'idnumber', get_string('idnumber', 'tool_lp'), 'maxlength="100"');
-        $mform->setType('idnumber', PARAM_TEXT);
+        $mform->setType('idnumber', PARAM_RAW);
         $mform->addRule('idnumber', null, 'required', null, 'client');
         $mform->addRule('idnumber', get_string('maximumchars', '', 100), 'maxlength', 100, 'client');
 
index 71f4af2..412419d 100644 (file)
@@ -144,6 +144,8 @@ $string['nfiles'] = '{$a} file(s)';
 $string['noactivities'] = 'No activities';
 $string['nocompetencies'] = 'No competencies have been created in this framework.';
 $string['nocompetenciesincourse'] = 'No competencies have been linked to this course.';
+$string['nocompetenciesinevidence'] = 'No competencies have been linked to this evidence.';
+$string['nocompetenciesinlearningplan'] = 'No competencies have been linked to this learning plan.';
 $string['nocompetenciesintemplate'] = 'No competencies have been linked to this learning plan template.';
 $string['nocompetencyframeworks'] = 'No competency frameworks have been created yet.';
 $string['nocompetencyselected'] = 'No competency selected';
index fab384d..701fc99 100644 (file)
@@ -6,13 +6,14 @@
 }
 .path-admin-tool-lp .progresstext {
     display: inline-block;
-    height: 40px;
     vertical-align: top;
 }
 .path-admin-tool-lp .progress {
     width: 100%;
     display: inline-block;
-    margin-right: 2em;
+}
+.path-admin-tool-lp .progress .bar {
+    min-width: 3em;
 }
 .dir-rtl.path-admin-tool-lp .progress .bar {
     float: right;
@@ -220,6 +221,10 @@ input[type="checkbox"].tool_lp_scale_proficient {
 .user-competency-course-navigation {
     width: 240px;
 }
+.user-competency-course-navigation span {
+    max-width: 100%;
+    overflow: hidden;
+}
 
 /** Competency grader */
 .competency-grader textarea {
index 0a51aef..d6ae8e6 100644 (file)
@@ -31,7 +31,7 @@
 
 <ul data-enhance="movetree" style="display: none;">
     <li>
-        <span>{{framework.shortname}}</span>
+        <span>{{{framework.shortname}}}</span>
         <ul>
             {{#competencies}}
                 {{> tool_lp/competencies_tree }}
index 5e30b8a..75c2282 100644 (file)
@@ -1,12 +1,12 @@
 <li data-id="{{id}}">
     {{#canmanage}}
     <span draggable="true">
-        {{shortname}}
+        {{{shortname}}}
     </span>
     {{/canmanage}}
     {{^canmanage}}
     <span>
-        {{shortname}}
+        {{{shortname}}}
     </span>
     {{/canmanage}}
     {{#haschildren}}
index c163870..d138fe1 100644 (file)
@@ -1,5 +1,5 @@
 <ul data-enhance="tree">
-    <li><span>{{shortname}}</span>
+    <li><span>{{{shortname}}}</span>
         <ul>
             {{#competencies}}
                 {{> tool_lp/competencies_tree }}
index 01e21c6..f756098 100644 (file)
 }}
 <nav id="competency-path-{{uniqid}}">
     <small>
-        <a href="{{pluginbaseurl}}/competencies.php?competencyframeworkid={{framework.id}}&pagecontextid={{pagecontextid}}" >{{framework.name}}</a>
+        <a href="{{pluginbaseurl}}/competencies.php?competencyframeworkid={{framework.id}}&pagecontextid={{pagecontextid}}" >{{{framework.name}}}</a>
         /
         {{#ancestors}}
-            <a data-action="competency-dialogue" href="#" data-id="{{id}}">{{name}}</a>
+            <a data-action="competency-dialogue" href="#" data-id="{{id}}">{{{name}}}</a>
             {{^last}}<span> / </span>{{/last}}
         {{/ancestors}}
     </small>
index fcb4464..916522b 100644 (file)
@@ -3,7 +3,7 @@
 <h3>{{#str}}competencyframeworks, tool_lp{{/str}}</h3>
 <select data-action="chooseframework">
 {{#frameworks}}
-<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{shortname}} <em>{{idnumber}}</em></option>
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{{shortname}}} <em>{{idnumber}}</em></option>
 {{/frameworks}}
 </select>
 {{/singleFramework}}
@@ -15,7 +15,7 @@
     <button>{{#pix}}a/search, ,{{#str}}search{{/str}}{{/pix}}</button>
 </form>
 <ul data-enhance="linktree" style="display: none;">
-    <li><span>{{framework.shortname}}</span>
+    <li><span>{{{framework.shortname}}}</span>
         <ul>
             {{#competencies}}
                 {{> tool_lp/competencies_tree }}
index f3f1ca0..e255dfd 100644 (file)
@@ -7,7 +7,7 @@
     <button>{{#pix}}a/search, ,{{#str}}search{{/str}}{{/pix}}</button>
 </form>
 <ul data-enhance="linktree" style="display: none;">
-    <li data-id="0"><span>{{framework.shortname}}</span>
+    <li data-id="0"><span>{{{framework.shortname}}}</span>
         <ul>
             {{#competencies}}
                 {{> tool_lp/competencies_tree }}
index 1e052b9..bddebc9 100644 (file)
@@ -3,7 +3,7 @@
     <h3>{{#str}}learningplans, tool_lp{{/str}}</h3>
     <select data-action="chooseplan">
     {{#plans}}
-        <option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{name}}</em></option>
+        <option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{{name}}}</option>
     {{/plans}}
     </select>
 {{/singlePlan}}
@@ -17,7 +17,7 @@
 </form>
 
 <ul data-enhance="linktree" style="display: none;">
-    <li><span>{{plan.name}}</span>
+    <li><span>{{{plan.name}}}</span>
         <ul>
             {{#competencies}}
                 {{> tool_lp/competencies_tree }}
index 490d1b8..d3eb766 100644 (file)
@@ -4,7 +4,7 @@
 <label for="competency-nav-{{uniqid}}" class="accesshide">{{#str}}jumptocompetency, tool_lp{{/str}}</label>
 <select id="competency-nav-{{uniqid}}">
 {{#competencies}}
-<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{shortname}} {{idnumber}}</option>
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{{shortname}}} {{idnumber}}</option>
 {{/competencies}}
 </select>
 </span>
index ec8c60f..907e731 100644 (file)
         <tbody>
             {{#children}}
             <tr data-competency="{{id}}">
-                <th scope="row">{{shortname}}</th>
-                <td><input type="number" min="0" value="{{points}}" name="points" aria-label="{{#str}}pointsgivenfor, tool_lp, {{competency.shortname}}{{/str}}"></td>
-                <td><input type="checkbox" value="1" name="required" {{#required}}checked{{/required}} aria-label="{{#str}}aisrequired, tool_lp, {{competency.shortname}}{{/str}}"></td>
+                <th scope="row">{{{shortname}}}</th>
+                <td>
+                    <label class="accesshide" for="pointsforcompetency-{{id}}">{{#str}}pointsgivenfor, tool_lp, {{{competency.shortname}}}{{/str}}</label>
+                    <input id="pointsforcompetency-{{id}}" type="number" min="0" value="{{points}}" name="points" />
+                </td>
+                <td>
+                    <label class="accesshide" for="competency-{{id}}-isrequired">{{#str}}aisrequired, tool_lp, {{{competency.shortname}}}{{/str}}</label>
+                    <input id="competency-{{id}}-isrequired" type="checkbox" value="1" name="required" {{#required}}checked{{/required}} />
+                </td>
             </tr>
             {{/children}}
         </tbody>
index 918b6ff..b1bca28 100644 (file)
@@ -1,10 +1,10 @@
 <div class='competency-heading'>
-    <h4 id="competency_link_{{competency.id}}">{{competency.shortname}}
+    <h4 id="competency_link_{{competency.id}}">{{{competency.shortname}}}
         <small>{{competency.idnumber}}</small>
     </h4>
     {{#framework}}
         <div class='competency-origin'>
-            <p><small>{{framework.shortname}} - {{taxonomyterm}}</small>
+            <p><small>{{{framework.shortname}}} - {{taxonomyterm}}</small>
         </div>
     {{/framework}}
 </div>
index 0a65312..674eed6 100644 (file)
@@ -66,7 +66,7 @@
             <a href="{{pluginbaseurl}}user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
                    id="competency-info-link-{{competency.id}}"
                    title="{{#str}}viewdetails, tool_lp{{/str}}">
-                <p><strong>{{competency.shortname}} <em>{{competency.idnumber}}</em></strong></p>
+                <p><strong>{{{competency.shortname}}} <em>{{competency.idnumber}}</em></strong></p>
             </a>
             <p>{{{competency.description}}}</p>
         {{/competency}}
 </tbody>
 </table>
 {{^competencies}}
-<p class="alert-info">
+<p class="alert alert-info">
     {{#str}}nocompetenciesincourse, tool_lp{{/str}}
 </p>
 {{/competencies}}
index 9202052..437bd53 100644 (file)
 {{#competencycount}}
 <div data-region="coursecompetencystatistics" class="well">
     {{#canbegradedincourse}}
-    <div class="clearfix">
-        <div class="span6">
-            <div class="progresstext">
+        {{< tool_lp/progress_bar}}
+            {{$progresstext}}
                 {{#str}}xcompetenciesproficientoutofyincourse, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
-            </div>
-        </div>
-        <div class="span6">
-            <span class="pull-right label label-info">{{proficientcompetencypercentageformatted}} %</span>
-            <div class="progress">
-                <div class="bar" style="width: {{proficientcompetencypercentage}}%;"></div>
-            </div>
-        </div>
-    </div>
+            {{/progresstext}}
+            {{$percentage}}{{proficientcompetencypercentage}}{{/percentage}}
+            {{$percentlabel}}{{proficientcompetencypercentageformatted}}&nbsp;%{{/percentlabel}}
+        {{/tool_lp/progress_bar}}
     {{/canbegradedincourse}}
     {{#canmanagecoursecompetencies}}
     {{#leastproficientcount}}
@@ -68,7 +62,7 @@
         <div>
             {{#leastproficient}}
                 <a href="#competency-info-link-{{id}}">
-                <div><p>{{shortname}} <em>{{idnumber}}</em></p></div>
+                <div><p>{{{shortname}}} <em>{{idnumber}}</em></p></div>
                 </a>
             {{/leastproficient}}
         </div>
index f0b656f..fce2b3d 100644 (file)
@@ -32,7 +32,7 @@
 }}
 <div data-region="managecompetencies">
 <h2>
-    {{framework.shortname}}
+    {{{framework.shortname}}}
     {{#canmanage}}
             <a href="{{pluginbaseurl}}/editcompetencyframework.php?id={{framework.id}}&pagecontextid={{pagecontextid}}&return=competencies">{{#pix}}t/edit, core, {{#str}}editcompetencyframework, tool_lp{{/str}}{{/pix}}</a>
     {{/canmanage}}
@@ -121,7 +121,7 @@ require(['tool_lp/tree', 'tool_lp/competencytree', 'tool_lp/competencyactions',
         function(ariatree, treeModel, actions, $) {
 
     treeModel.init({{framework.id}},
-                   '{{framework.shortname}}',
+                   {{#quote}} {{{framework.shortname}}} {{/quote}},
                    '{{search}}',
                    '[data-enhance=tree]',
                    {{canmanage}});
index ee99906..a901f4a 100644 (file)
@@ -34,7 +34,7 @@
     * navigation - array of strings containing buttons for navigation
 }}
 <div data-region="managecompetencies">
-<div class="btn-group pull-left">
+<div class="pull-left">
     {{#navigation}}
     {{{.}}}
     {{/navigation}}
@@ -52,7 +52,7 @@
     <tbody class="drag-parentnode">
         {{#competencyframeworks}}
         <tr class="drag-samenode" data-frameworkid="{{id}}">
-            <td><span class="drag-handlecontainer"></span><span><a href="{{pluginbaseurl}}/competencies.php?competencyframeworkid={{id}}&amp;pagecontextid={{pagecontextid}}">{{shortname}} ({{idnumber}})</a></span> {{^visible}}{{#str}}hiddenhint, tool_lp{{/str}}{{/visible}}</td>
+            <td><span class="drag-handlecontainer"></span><span><a href="{{pluginbaseurl}}/competencies.php?competencyframeworkid={{id}}&amp;pagecontextid={{pagecontextid}}">{{{shortname}}} ({{idnumber}})</a></span> {{^visible}}{{#str}}hiddenhint, tool_lp{{/str}}{{/visible}}</td>
             <td>{{competenciescount}}</td>
             <td>{{contextnamenoprefix}}</td>
             <td>
@@ -86,7 +86,7 @@
     </tbody>
 </table>
 {{^competencyframeworks}}
-<p class="alert-info">
+<p class="alert alert-info">
     {{#str}}nocompetencyframeworks, tool_lp{{/str}}
 </p>
 {{/competencyframeworks}}
index 82d2b76..fbe00d6 100644 (file)
@@ -32,7 +32,7 @@
     * navigation - array of strings containing buttons for navigation
 }}
 <div data-region="managetemplates">
-<div class="btn-group pull-left">
+<div class="pull-left">
     {{#navigation}}
     {{{.}}}
     {{/navigation}}
@@ -53,7 +53,7 @@
     <tbody class="drag-parentnode">
         {{#templates}}
         <tr class="drag-samenode" data-templateid="{{id}}">
-            <td><a href="{{pluginbaseurl}}/templatecompetencies.php?templateid={{id}}&amp;pagecontextid={{pagecontextid}}">{{shortname}}</a></span> {{^visible}}{{#str}}hiddenhint, tool_lp{{/str}}{{/visible}}</td>
+            <td><a href="{{pluginbaseurl}}/templatecompetencies.php?templateid={{id}}&amp;pagecontextid={{pagecontextid}}">{{{shortname}}}</a></span> {{^visible}}{{#str}}hiddenhint, tool_lp{{/str}}{{/visible}}</td>
             <td>{{contextnamenoprefix}}</td>
             <td><a class="template-cohorts" href="{{pluginbaseurl}}/template_cohorts.php?id={{id}}&amp;pagecontextid={{pagecontextid}}">{{cohortscount}}</a></td>
             <td><a class="template-userplans" href="{{pluginbaseurl}}/template_plans.php?id={{id}}&amp;pagecontextid={{pagecontextid}}">{{planscount}}</a></td>
@@ -98,7 +98,7 @@
     </tbody>
 </table>
 {{^templates}}
-<p class="alert-info">
+<p class="alert alert-info">
     {{#str}}notemplates, tool_lp{{/str}}
 </p>
 {{/templates}}
index d93b418..40eced3 100644 (file)
@@ -1,3 +1,3 @@
-<p class="alert-error">
+<p class="alert alert-error">
     {{#str}}nocompetencyframeworks, tool_lp{{/str}}
 </p>
index 6861674..20eb2bb 100644 (file)
@@ -19,7 +19,7 @@
 }}
 <div data-region="plan-page" data-id="{{plan.id}}" data-userid="{{plan.userid}}">
     <h2>
-        {{plan.name}}
+        {{{plan.name}}}
         {{#plan.canbeedited}}
             <a href="{{pluginbaseurl}}/editplan.php?id={{plan.id}}&amp;userid={{plan.userid}}">{{#pix}}t/edit, core, {{#str}}editplan, tool_lp{{/str}}{{/pix}}</a>
         {{/plan.canbeedited}}
@@ -79,7 +79,7 @@
                     {{#canread}}
                         <a href="{{pluginbaseurl}}/templatecompetencies.php?templateid={{id}}&amp;pagecontextid={{contextid}}">
                     {{/canread}}
-                    {{plan.template.shortname}}{{#canread}}</a>{{/canread}}
+                    {{{plan.template.shortname}}}{{#canread}}</a>{{/canread}}
                     {{#plan.isunlinkallowed}}
                         (<a data-action="plan-unlink" href="#">{{#str}}unlinkplantemplate, tool_lp{{/str}}</a>)
                     {{/plan.isunlinkallowed}}
             {{/description}}
             <dt>{{#str}}progress, tool_lp{{/str}}</dt>
             <dd>
-                <div class="span4">
-                    <div class="progresstext">
-                        {{#str}}xcompetenciesproficientoutofy, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" }{{/str}}
-                    </div>
-                </div>
-                <div class="span4">
-                    <span class="pull-right label label-info">{{proficientcompetencypercentageformatted}} %</span>
-                    <div class="progress">
-                        <div class="bar" style="width: {{proficientcompetencypercentage}}%;"></div>
-                    </div>
-                </div>
+            {{< tool_lp/progress_bar}}
+                {{$progresstext}}
+                    {{#str}}xcompetenciesproficientoutofy, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" }{{/str}}
+                {{/progresstext}}
+                {{$percentage}}{{proficientcompetencypercentage}}{{/percentage}}
+                {{$percentlabel}}{{proficientcompetencypercentageformatted}}&nbsp;%{{/percentlabel}}
+            {{/tool_lp/progress_bar}}
             </dd>
         </dl>
     </div>
                         {{#plan.canbeedited}}
                         <span class="drag-handlecontainer pull-left"></span>
                         {{/plan.canbeedited}}
-                        <a data-usercompetency="true" href="#">{{competency.shortname}}</a>
+                        <a data-usercompetency="true" href="#">{{{competency.shortname}}}</a>
                         <em>{{competency.idnumber}}</em>
                         {{#comppath}}
                             <br>
                 {{/competencies}}
             </tbody>
         </table>
+        {{^competencies}}
+            <p class="alert alert-info">
+                {{#str}}nocompetenciesinlearningplan, tool_lp{{/str}}
+            </p>
+        {{/competencies}}
     </div>
 </div>
 {{#js}}
index 78d1c1d..94226c9 100644 (file)
@@ -34,7 +34,7 @@
 }}
 
 <div data-region="plans">
-<div class="btn-group pull-left">
+<div class="pull-left">
     {{#navigation}}
     {{{.}}}
     {{/navigation}}
@@ -53,7 +53,7 @@
         {{#plans}}
         <tr data-region="plan-node" data-id="{{id}}" data-userid="{{userid}}">
             <td>
-                <span><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{name}}</a></span>
+                <span><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></span>
             </td>
              <td>
                {{#isbasedontemplate}}
 </table>
 
 {{^plans}}
-<p class="alert-info">
+<p class="alert alert-info">
     {{#str}}nouserplans, tool_lp{{/str}}
 </p>
 {{/plans}}
diff --git a/admin/tool/lp/templates/progress_bar.mustache b/admin/tool/lp/templates/progress_bar.mustache
new file mode 100644 (file)
index 0000000..118f761
--- /dev/null
@@ -0,0 +1,57 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_lp/progress_bar
+
+    Moodle progress bar template for tool_lp.
+
+    The purpose of this template is to render a progress bar with a brief description.
+    Inherits core/columns-1to2.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * progresstextvalue A brief text that describes the progress bar.
+    * percentagevalue The numeric value for the progress bar's percentage.
+    * percentlabelvalue The progress bar's label that shows the percentage value.
+
+    Example context (json):
+    {
+        "progresstextvalue": "Progress:",
+        "percentagevalue": "50",
+        "percentlabelvalue": "50.00 %"
+    }
+
+}}
+<div class="row-fluid rtl-compatible">
+    <div class="span4">
+        <div class="progresstext">
+            {{$progresstext}}{{progresstextvalue}}{{/progresstext}}
+        </div>
+    </div>
+    <div class="span8">
+        <div class="progress">
+            <div class="bar" style="width: {{$percentage}}{{percentagevalue}}{{/percentage}}%;" role="progressbar" aria-valuenow="{{$percentage}}{{percentagevalue}}{{/percentage}}" aria-valuemin="0" aria-valuemax="100">
+                {{$percentlabel}}{{percentlabelvalue}}{{/percentlabel}}
+            </div>
+        </div>
+    </div>
+</div>
index f9b2c51..71967d7 100644 (file)
@@ -12,7 +12,7 @@
             {{/showdeleterelatedaction}}
             <p>
                 <a href="#" data-action="competency-dialogue" data-id="{{id}}">
-                    {{shortname}}{{#idnumber}} {{idnumber}}{{/idnumber}}
+                    {{{shortname}}}{{#idnumber}} {{idnumber}}{{/idnumber}}
                 </a>
             </p>
         </li>
index 0cd3267..a2ebaef 100644 (file)
@@ -19,7 +19,7 @@
 }}
 <div data-region="templatecompetenciespage">
     <h2>
-        {{template.shortname}}
+        {{{template.shortname}}}
         {{#template.canmanage}}
             <a href="{{pluginbaseurl}}/edittemplate.php?id={{template.id}}&amp;pagecontextid={{pagecontextid}}">{{#pix}}t/edit, core, {{#str}}edittemplate, tool_lp{{/str}}{{/pix}}</a>
         {{/template.canmanage}}
@@ -55,7 +55,7 @@
                         {{#hascourses}}
                         <ul class="inline">
                         {{#linkedcourses}}
-                            <li><a href="{{viewurl}}?id={{id}}">{{fullname}} ({{shortname}})</a></li>
+                            <li><a href="{{viewurl}}?id={{id}}">{{{fullname}}} ({{{shortname}}})</a></li>
                         {{/linkedcourses}}
                         </ul>
                         {{/hascourses}}
@@ -68,7 +68,7 @@
             </div>
         </div>
         {{^competencies}}
-        <p class="alert-info">
+        <p class="alert alert-info">
             {{#str}}nocompetenciesintemplate, tool_lp{{/str}}
         </p>
         {{/competencies}}
index 66caf50..91b4512 100644 (file)
 }}
 {{#competencycount}}
 <div data-region="templatestatistics" class="well">
-    <div class="clearfix">
-        <div class="span4">
-            <div class="progresstext">
-                {{#str}}xcompetencieslinkedoutofy, tool_lp, { "x": "{{linkedcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
-            </div>
-        </div>
-        <div class="span6">
-            <span class="pull-right label label-info">{{linkedcompetencypercentageformatted}} %</span>
-            <div class="progress">
-                <div class="bar" style="width: {{linkedcompetencypercentage}}%;"></div>
-            </div>
-        </div>
-    </div>
+    {{< tool_lp/progress_bar}}
+        {{$progresstext}}
+            {{#str}}xcompetencieslinkedoutofy, tool_lp, { "x": "{{linkedcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
+        {{/progresstext}}
+        {{$percentage}}{{linkedcompetencypercentage}}{{/percentage}}
+        {{$percentlabel}}{{linkedcompetencypercentageformatted}}&nbsp;%{{/percentlabel}}
+    {{/tool_lp/progress_bar}}
     {{#plancount}}
-    <div class="clearfix">
-        <div class="span4">
-            <div class="progresstext">
+        {{< tool_lp/progress_bar}}
+            {{$progresstext}}
                 {{#str}}xplanscompletedoutofy, tool_lp, { "x": "{{completedplancount}}", "y": "{{plancount}}" } {{/str}}
-            </div>
-        </div>
-        <div class="span6">
-            <span class="pull-right label label-info">{{completedplanpercentageformatted}} %</span>
-            <div class="progress">
-                <div class="bar" style="width: {{completedplanpercentage}}%;">
-
-                </div>
-            </div>
-        </div>
-    </div>
+            {{/progresstext}}
+            {{$percentage}}{{completedplanpercentage}}{{/percentage}}
+            {{$percentlabel}}{{completedplanpercentageformatted}}&nbsp;%{{/percentlabel}}
+        {{/tool_lp/progress_bar}}
     {{/plancount}}
     {{#usercompetencyplancount}}
-    <div  class="clearfix">
-        <div class="span4">
-            <div class="progresstext">
+        {{< tool_lp/progress_bar}}
+            {{$progresstext}}
                 {{#str}}averageproficiencyrate, tool_lp, {{proficientusercompetencyplanpercentageformatted}} {{/str}}
-            </div>
-        </div>
-        <div class="span6">
-            <span class="pull-right label label-info">{{proficientusercompetencyplanpercentageformatted}} %</span>
-            <div class="progress">
-                <div class="bar" style="width: {{proficientusercompetencyplanpercentage}}%;"></div>
-            </div>
-        </div>
-    </div>
+            {{/progresstext}}
+            {{$percentage}}{{proficientusercompetencyplanpercentage}}{{/percentage}}
+            {{$percentlabel}}{{proficientusercompetencyplanpercentageformatted}}&nbsp;%{{/percentlabel}}
+        {{/tool_lp/progress_bar}}
     {{/usercompetencyplancount}}
     {{#leastproficientcount}}
     <div>
                 {{#showcompetencylinks}}
                 <a href="#competency_link_{{id}}">
                 {{/showcompetencylinks}}
-                <div><p>{{shortname}} <em>{{idnumber}}</em></p></div>
+                <div><p>{{{shortname}}} <em>{{idnumber}}</em></p></div>
                 {{#showcompetencylinks}}
                 </a>
                 {{/showcompetencylinks}}
index 4b7770f..a81c0f8 100644 (file)
@@ -17,7 +17,7 @@
 <label for="competency-nav-{{uniqid}}" class="accesshide">{{#str}}jumptocompetency, tool_lp{{/str}}</label>
 <select id="competency-nav-{{uniqid}}">
 {{#competencies}}
-<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{shortname}} {{idnumber}}</option>
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{{shortname}}} {{idnumber}}</option>
 {{/competencies}}
 </select>
 </span>
index 4b11153..a0e14bf 100644 (file)
@@ -33,7 +33,7 @@
 }}
 
 <div data-region="user-evidence-list">
-<div class="btn-group pull-left">
+<div class="pull-left">
     {{#navigation}}
         {{{.}}}
     {{/navigation}}
@@ -53,7 +53,7 @@
     <tbody>
         {{#evidence}}
         <tr data-region='user-evidence-node' data-id="{{id}}" data-userid="{{userid}}">
-            <td><a href="{{pluginbaseurl}}/user_evidence.php?id={{id}}">{{name}}</a></td>
+            <td><a href="{{pluginbaseurl}}/user_evidence.php?id={{id}}">{{{name}}}</a></td>
             <td>
                 {{^hasurlorfiles}}
                     -
@@ -77,7 +77,7 @@
                     <ul class="user-evidence-competencies">
                     {{#usercompetencies}}
                         <li>
-                            {{competency.shortname}} <small><em>{{competency.idnumber}}</em></small> ({{usercompetency.statusname}}{{#usercompetency.reviewer.fullname}} / {{usercompetency.reviewer.fullname}}{{/usercompetency.reviewer.fullname}})
+                            {{{competency.shortname}}} <small><em>{{competency.idnumber}}</em></small> ({{usercompetency.statusname}}{{#usercompetency.reviewer.fullname}} / {{usercompetency.reviewer.fullname}}{{/usercompetency.reviewer.fullname}})
                         </li>
                     {{/usercompetencies}}
                     </ul>
 </table>
 
 {{^evidence}}
-    <p class="alert-info">{{#str}}nouserevidence, tool_lp{{/str}}</p>
+    <p class="alert alert-info">{{#str}}nouserevidence, tool_lp{{/str}}</p>
 {{/evidence}}
 
 </div>
index 40876be..29f28cf 100644 (file)
@@ -31,7 +31,7 @@
 {{#userevidence}}
 <div data-region="user-evidence-page" data-id="{{id}}" data-userid="{{userid}}">
     <h2>
-        {{name}}
+        {{{name}}}
         {{#canmanage}}
             <a href="{{pluginbaseurl}}/user_evidence_edit.php?id={{id}}&amp;userid={{userid}}">{{#pix}}t/edit, core, {{#str}}editthisuserevidence, tool_lp{{/str}}{{/pix}}</a>
         {{/canmanage}}
@@ -78,7 +78,7 @@
                 {{#usercompetencies}}
                 <tr data-id="{{competency.id}}">
                     <td>
-                        <a href="{{pluginbaseurl}}/user_competency.php?id={{usercompetency.id}}" data-id="{{usercompetency.id}}">{{competency.shortname}}</a>
+                        <a href="{{pluginbaseurl}}/user_competency.php?id={{usercompetency.id}}" data-id="{{usercompetency.id}}">{{{competency.shortname}}}</a>
                     </td>
                      <td>
                         {{usercompetency.statusname}} {{#usercompetency.reviewer.fullname}} / {{usercompetency.reviewer.fullname}}{{/usercompetency.reviewer.fullname}}
                 {{/usercompetencies}}
             </tbody>
         </table>
+        {{^usercompetencies}}
+            <p class="alert alert-info">
+                {{#str}}nocompetenciesinevidence, tool_lp{{/str}}
+            </p>
+        {{/usercompetencies}}
     </div>
 </div>
 
index caebb22..159ec86 100644 (file)
Binary files a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-debug.js and b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-debug.js differ
index f91699e..47c197c 100644 (file)
Binary files a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js and b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js differ
index caebb22..159ec86 100644 (file)
Binary files a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder.js and b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder.js differ
index c2feccb..3624631 100644 (file)
@@ -63,5 +63,5 @@ Y.extend(DRAGREORDER, M.core.dragdrop, {
 
 M.tool_lp = M.tool_lp || {};
 M.tool_lp.dragdrop_reorder = function(params) {
-    new DRAGREORDER(params);
+    return new DRAGREORDER(params);
 };
index a5527c2..a444850 100644 (file)
     <h4>{{#str}}unmappedin, tool_lpmigrate, {{frameworkfrom.shortname}}{{/str}}</h4>
     <ul>
         {{#unmappedfrom}}
-            <li><a href="#" data-id="{{id}}" data-action="competency-dialogue" data-includecourses="true">{{shortname}}</a> <em>{{idnumber}}</em></li>
+            <li><a href="#" data-id="{{id}}" data-action="competency-dialogue" data-includecourses="true">{{{shortname}}}</a> <em>{{idnumber}}</em></li>
         {{/unmappedfrom}}
     </ul>
     {{/hasunmappedfrom}}
     <h4>{{#str}}unmappedin, tool_lpmigrate, {{frameworkto.shortname}}{{/str}}</h4>
     <ul>
         {{#unmappedto}}
-            <li><a href="#" data-id="{{id}}" data-action="competency-dialogue" data-includecourses="true">{{shortname}}</a> <em>{{idnumber}}</em></li>
+            <li><a href="#" data-id="{{id}}" data-action="competency-dialogue" data-includecourses="true">{{{shortname}}}</a> <em>{{idnumber}}</em></li>
         {{/unmappedto}}
     </ul>
     {{/hasunmappedto}}
index 06b06de..2acb3b2 100644 (file)
@@ -126,6 +126,9 @@ class category_bin extends base_bin {
             throw new \moodle_exception('Failed to backup activity prior to deletion.');
         }
 
+        // Have finished with the controller, let's destroy it, freeing mem and resources.
+        $controller->destroy();
+
         // Grab the filename.
         $file = $result['backup_destination'];
         if (!$file->get_contenthash()) {
@@ -259,6 +262,9 @@ class category_bin extends base_bin {
         // Run the import.
         $controller->execute_plan();
 
+        // Have finished with the controller, let's destroy it, freeing mem and resources.
+        $controller->destroy();
+
         // Fire event.
         $event = \tool_recyclebin\event\category_bin_item_restored::create(array(
             'objectid' => $item->id,
index a05a7cf..67e1a21 100644 (file)
@@ -130,6 +130,9 @@ class course_bin extends base_bin {
             throw new \moodle_exception('Failed to backup activity prior to deletion.');
         }
 
+        // Have finished with the controller, let's destroy it, freeing mem and resources.
+        $controller->destroy();
+
         // Grab the filename.
         $file = $result['backup_destination'];
         if (!$file->get_contenthash()) {
@@ -246,6 +249,9 @@ class course_bin extends base_bin {
         // Run the import.
         $controller->execute_plan();
 
+        // Have finished with the controller, let's destroy it, freeing mem and resources.
+        $controller->destroy();
+
         // Fire event.
         $event = \tool_recyclebin\event\course_bin_item_restored::create(array(
             'objectid' => $item->id,
index 4486cde..7b111cb 100644 (file)
@@ -740,7 +740,6 @@ class tool_uploadcourse_course {
                 $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
             }
             $rc->destroy();
-            unset($rc); // File logging is a mess, we can only try to rely on gc to close handles.
         }
 
         // Proceed with enrolment data.
index 20bbaa3..0b62e87 100644 (file)
@@ -35,13 +35,6 @@ global $CFG;
  */
 class tool_uploadcourse_course_testcase extends advanced_testcase {
 
-    /**
-     * Tidy up open files that may be left open.
-     */
-    protected function tearDown() {
-        gc_collect_cycles();
-    }
-
     public function test_proceed_without_prepare() {
         $this->resetAfterTest(true);
         $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
index 41a838c..6768507 100644 (file)
@@ -129,7 +129,6 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($result['backup_destination']));
         $c1backupfile = $result['backup_destination']->copy_content_to_temp();
         $bc->destroy();
-        unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
         // Creating backup file.
         $bc = new backup_controller(backup::TYPE_1COURSE, $c2->id, backup::FORMAT_MOODLE,
@@ -139,7 +138,6 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($result['backup_destination']));
         $c2backupfile = $result['backup_destination']->copy_content_to_temp();
         $bc->destroy();
-        unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
         $oldcfg = isset($CFG->keeptempdirectoriesonbackup) ? $CFG->keeptempdirectoriesonbackup : false;
         $CFG->keeptempdirectoriesonbackup = true;
index 2d2ec04..1dd20dc 100644 (file)
@@ -36,13 +36,6 @@ require_once($CFG->libdir . '/csvlib.class.php');
  */
 class tool_uploadcourse_processor_testcase extends advanced_testcase {
 
-    /**
-     * Tidy up open files that may be left open.
-     */
-    protected function tearDown() {
-        gc_collect_cycles();
-    }
-
     public function test_basic() {
         global $DB;
         $this->resetAfterTest(true);
index 30536e6..75b0d84 100644 (file)
@@ -937,7 +937,9 @@ class auth_plugin_ldap extends auth_plugin_base {
                 // It isn't possible to just rely on the configured suspension attribute since
                 // things like active directory use bit masks, other things using LDAP might
                 // do different stuff as well.
-                $user->suspended = $this->is_user_suspended($user);
+                //
+                // The cast to int is a workaround for MDL-53959.
+                $user->suspended = (int)$this->is_user_suspended($user);
                 if (empty($user->lang)) {
                     $user->lang = $CFG->lang;
                 }
@@ -1012,7 +1014,8 @@ class auth_plugin_ldap extends auth_plugin_base {
             if (!empty($updatekeys)) {
                 $newuser = new stdClass();
                 $newuser->id = $userid;
-                $newuser->suspended = $this->is_user_suspended((object) $newinfo);
+                // The cast to int is a workaround for MDL-53959.
+                $newuser->suspended = (int)$this->is_user_suspended((object) $newinfo);
 
                 foreach ($updatekeys as $key) {
                     if (isset($newinfo[$key])) {
index 9b1d030..b7c00ff 100644 (file)
@@ -22,5 +22,5 @@
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['auth_ltidescription'] = 'The LTI authentication plugin enables your site to behave as an LTI provider - this plugin works in conjunction with the LTI enrolment plugin by allowing external users to access a course or individual activities.';
+$string['auth_ltidescription'] = 'The LTI authentication plugin, together with the \'Publish as LTI tool\' enrolment plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.';
 $string['pluginname'] = 'LTI';
index e270f9e..35edd9c 100644 (file)
@@ -158,6 +158,8 @@ class backup_controller extends base_controller {
     public function destroy() {
         // Only need to destroy circulars under the plan. Delegate to it.
         $this->plan->destroy();
+        // Loggers may have also chained references, destroy them. Also closing resources when needed.
+        $this->logger->destroy();
     }
 
     public function finish_ui() {
@@ -184,7 +186,7 @@ class backup_controller extends base_controller {
             $this->save_controller();
             $tbc = self::load_controller($this->backupid);
             $this->logger = $tbc->logger; // wakeup loggers
-            $tbc->destroy(); // Clean temp controller structures
+            $tbc->plan->destroy(); // Clean plan controller structures, keeping logger alive.
 
         } else if ($status == backup::STATUS_FINISHED_OK) {
             // If the operation has ended without error (backup::STATUS_FINISHED_OK)
index 8c72c58..2829b99 100644 (file)
@@ -170,6 +170,8 @@ class restore_controller extends base_controller {
     public function destroy() {
         // Only need to destroy circulars under the plan. Delegate to it.
         $this->plan->destroy();
+        // Loggers may have also chained references, destroy them. Also closing resources when needed.
+        $this->logger->destroy();
     }
 
     public function finish_ui() {
@@ -196,7 +198,7 @@ class restore_controller extends base_controller {
             $this->save_controller();
             $tbc = self::load_controller($this->restoreid);
             $this->logger = $tbc->logger; // wakeup loggers
-            $tbc->destroy(); // Clean temp controller structures
+            $tbc->plan->destroy(); // Clean plan controller structures, keeping logger alive.
 
         } else if ($status == backup::STATUS_FINISHED_OK) {
             // If the operation has ended without error (backup::STATUS_FINISHED_OK)
index 3c1f547..158249b 100644 (file)
@@ -493,6 +493,8 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
+        // The function floatval will return a float even if there is text mixed with the release number.
+        $backuprelease = floatval($this->get_task()->get_info()->backup_release);
 
         // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
         if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
@@ -504,6 +506,14 @@ class restore_gradebook_structure_step extends restore_structure_step {
             require_once($CFG->libdir . '/db/upgradelib.php');
             upgrade_calculated_grade_items($this->get_courseid());
         }
+        // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
+        // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
+        // be checked for this problem.
+        if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || $backuprelease <= 2.9)) {
+            require_once($CFG->libdir . '/db/upgradelib.php');
+            upgrade_course_letter_boundary($this->get_courseid());
+        }
+
     }
 
     /**
index 47b9971..bac5859 100644 (file)
@@ -39,13 +39,6 @@ require_once($CFG->libdir . '/completionlib.php');
  */
 class core_backup_moodle2_course_format_testcase extends advanced_testcase {
 
-    /**
-     * Tidy up open files that may be left open.
-     */
-    protected function tearDown() {
-        gc_collect_cycles();
-    }
-
     /**
      * Tests a backup and restore adds the required section option data
      * when the same course format is used.
@@ -269,4 +262,4 @@ class format_test_cs2_options extends format_test_cs_options {
             ),
         ) + parent::section_format_options($foreditform);
     }
-}
\ No newline at end of file
+}
index 4842d4a..5231082 100644 (file)
@@ -38,13 +38,6 @@ require_once($CFG->libdir . '/completionlib.php');
  */
 class core_backup_moodle2_testcase extends advanced_testcase {
 
-    /**
-     * Tidy up open files that may be left open.
-     */
-    protected function tearDown() {
-        gc_collect_cycles();
-    }
-
     /**
      * Tests the availability field on modules and sections is correctly
      * backed up and restored.
@@ -161,11 +154,6 @@ class core_backup_moodle2_testcase extends advanced_testcase {
                     $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n";
         }
 
-        // Must set restore_controller variable to null so that php
-        // garbage-collects it; otherwise the file will be left open and
-        // attempts to delete it will cause a permission error on Windows
-        // systems, breaking unit tests.
-        $rc = null;
         $this->assertNull($thrown);
 
         // Get information about the resulting course and check that it is set
index 31e7f03..88a9c88 100644 (file)
@@ -1,6 +1,14 @@
 This files describes API changes in /backup/*,
 information provided here is intended especially for developers.
 
+=== 3.1 ===
+
+* New close() method added to loggers so they can close any open resource. Previously
+  any backup and restore operation using the file logger may be leaving unclosed files.
+* New destroy() method added to loggers, normally called from backup and restore controllers
+  own destroy() method to ensure that all references in the chained loggers are deleted
+  and any open resource within them is closed properly.
+
 === 3.0 ===
 
 * The backup_auto_keep setting, in automated backups configuration, is now
index 6bcbda4..8e4ced3 100644 (file)
@@ -131,6 +131,7 @@ class backup_check_testcase extends advanced_testcase {
             backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
         $this->assertTrue(backup_check::check_security($bc, true));
         $this->assertTrue($bc instanceof backup_controller);
+        $bc->destroy();
 
     }
 }
index 120ea6c..6d48d36 100644 (file)
@@ -101,9 +101,11 @@ abstract class restore_dbops {
 
             // If included, add it
             if ($included) {
-                $includedtasks[] = $task;
+                $includedtasks[] = clone($task); // A clone is enough. In fact we only need the basepath.
             }
         }
+        $rc->destroy(); // Always need to destroy.
+
         return $includedtasks;
     }
 
@@ -1510,8 +1512,12 @@ abstract class restore_dbops {
         // Calculate the context we are going to use for capability checking
         $context = context_course::instance($courseid);
 
+        // TODO: Some day we must kill this dependency and change the process
+        // to pass info around without loading a controller copy.
         // When conflicting users are detected we may need original site info.
-        $restoreinfo = restore_controller_dbops::load_controller($restoreid)->get_info();
+        $rc = restore_controller_dbops::load_controller($restoreid);
+        $restoreinfo = $rc->get_info();
+        $rc->destroy(); // Always need to destroy.
 
         // Calculate if we have perms to create users, by checking:
         // to 'moodle/restore:createuser' and 'moodle/restore:userinfo'
index 4096371..a5a7faa 100644 (file)
@@ -302,6 +302,7 @@ abstract class backup_helper {
                     $bc = backup_controller::load_controller($backupid);
                     $bc->log('Attempt to copy backup file to the specified directory using filesystem failed - ',
                             backup::LOG_WARNING, $dir);
+                    $bc->destroy();
                 }
                 // bad luck, try to deal with the file the old way - keep backup in file area if we can not copy to ext system
             }
index ed2125c..32b0c06 100644 (file)
@@ -71,6 +71,30 @@ abstract class base_logger implements checksumable {
         return $this->level;
     }
 
+    /**
+     * Destroy (nullify) the chain of loggers references, also closing resources when needed.
+     *
+     * @since Moodle 3.1
+     */
+    public final function destroy() {
+        // Recursively destroy the chain.
+        if ($this->next !== null) {
+            $this->next->destroy();
+            $this->next = null;
+        }
+        // And close every logger.
+        $this->close();
+    }
+
+    /**
+     * Close any resource the logger may have open.
+     *
+     * @since Moodle 3.1
+     */
+    public function close() {
+        // Nothing to do by default. Only loggers using resources (files, own connections...) need to override this.
+    }
+
 // checksumable interface methods
 
     public function calculate_checksum() {
index 5c05380..98ff74a 100644 (file)
@@ -66,6 +66,19 @@ class file_logger extends base_logger {
         }
     }
 
+    /**
+     * Close the logger resources (file handle) if still open.
+     *
+     * @since Moodle 3.1
+     */
+    public function close() {
+        // Close the file handle if hasn't been closed already.
+        if (is_resource($this->fhandle)) {
+            fclose($this->fhandle);
+            $this->fhandle = null;
+        }
+    }
+
 // Protected API starts here
 
     protected function action($message, $level, $options = null) {
index 67f8e74..70a65f9 100644 (file)
@@ -108,6 +108,16 @@ class backup_logger_testcase extends basic_testcase {
         $this->assertEquals($lo1->get_levelstr(backup::LOG_WARNING), 'warn');
         $this->assertEquals($lo1->get_levelstr(backup::LOG_INFO), 'info');
         $this->assertEquals($lo1->get_levelstr(backup::LOG_DEBUG), 'debug');
+
+        // Test destroy.
+        $lo1 = new mock_base_logger1(backup::LOG_ERROR);
+        $lo2 = new mock_base_logger2(backup::LOG_ERROR);
+        $lo1->set_next($lo2);
+        $this->assertInstanceOf('base_logger', $lo1->get_next());
+        $this->assertNull($lo2->get_next());
+        $lo1->destroy();
+        $this->assertNull($lo1->get_next());
+        $this->assertNull($lo2->get_next());
     }
 
     /**
@@ -249,9 +259,9 @@ class backup_logger_testcase extends basic_testcase {
         $result = $lo2->process($message2, backup::LOG_WARNING, $options);
         $this->assertTrue($result);
 
-        // Destruct loggers
-        $lo1 = null;
-        $lo2 = null;
+        // Destroy loggers.
+        $lo1->destroy();
+        $lo2->destroy();
 
         // Load file results to analyze them
         $fcontents = file_get_contents($file);
@@ -275,6 +285,7 @@ class backup_logger_testcase extends basic_testcase {
         $this->assertTrue(file_exists($file));
         $message = 'testing file_logger';
         $result = $lo->process($message, backup::LOG_ERROR, $options);
+        $lo->close(); // Closes logger.
         // Get file contents and inspect them
         $fcontents = file_get_contents($file);
         $this->assertTrue($result);
@@ -282,7 +293,6 @@ class backup_logger_testcase extends basic_testcase {
         $this->assertTrue(strpos($fcontents, '[error]') !== false);
         $this->assertTrue(strpos($fcontents, '&nbsp;&nbsp;') !== false);
         $this->assertTrue(substr_count($fcontents , '] ') >= 2);
-        $lo->__destruct(); // closes file handle
         unlink($file); // delete file
 
         // Instantiate, write something, force deletion, try to write again
@@ -292,7 +302,8 @@ class backup_logger_testcase extends basic_testcase {
         $this->assertTrue(file_exists($file));
         $message = 'testing file_logger';
         $result = $lo->process($message, backup::LOG_ERROR);
-        fclose($lo->get_fhandle()); // close file
+        $lo->close();
+        $this->assertNull($lo->get_fhandle());
         try {
             $result = @$lo->process($message, backup::LOG_ERROR); // Try to write again
             $this->assertTrue(false, 'base_logger_exception expected');
@@ -326,6 +337,7 @@ class backup_logger_testcase extends basic_testcase {
         $lo = new file_logger(backup::LOG_NONE, true, true, $file);
         $this->assertTrue($lo instanceof file_logger);
         $this->assertFalse(file_exists($file));
+        $lo->close();
 
         // Remove the test dir and any content
         @remove_dir(dirname($file));
index bfc98f3..3e9b2fe 100644 (file)
@@ -89,6 +89,8 @@ class backup_plan_testcase extends advanced_testcase {
         // Calculate checksum and check it
         $checksum = $bp->calculate_checksum();
         $this->assertTrue($bp->is_checksum_correct($checksum));
+
+        $bc->destroy();
     }
 
     /**
index 826b349..0b1ba6d 100644 (file)
@@ -88,6 +88,7 @@ class backup_step_testcase extends advanced_testcase {
         $this->assertTrue($bs instanceof backup_step);
         $this->assertEquals($bs->get_name(), 'stepname');
 
+        $bc->destroy();
     }
 
     /**
@@ -128,6 +129,8 @@ class backup_step_testcase extends advanced_testcase {
         $this->assertTrue(strpos($contents, '<field2>value2</field2>') !== false);
         $this->assertTrue(strpos($contents, '</test>') !== false);
 
+        $bc->destroy();
+
         unlink($file); // delete file
 
         // Remove the test dir and any content
index efc1b60..40ee6ea 100644 (file)
@@ -93,6 +93,7 @@ class backup_task_testcase extends advanced_testcase {
         $checksum = $bt->calculate_checksum();
         $this->assertTrue($bt->is_checksum_correct($checksum));
 
+        $bc->destroy();
     }
 
     /**
diff --git a/blocks/completionstatus/tests/behat/block_completionstatus.feature b/blocks/completionstatus/tests/behat/block_completionstatus.feature
new file mode 100644 (file)
index 0000000..58b7cc7
--- /dev/null
@@ -0,0 +1,57 @@
+@block @block_completionstatus
+Feature: Enable Block Completion in a course
+  In order to view the completion block in a course
+  As a teacher
+  I can add completion block to a course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And the following "courses" exist:
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+
+  Scenario: Add the block to a the course where completion is disabled
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the following fields to these values:
+      | Enable completion tracking | No |
+    And I press "Save and display"
+    When I add the "Course completion status" block
+    Then I should see "Completion is not enabled for this course" in the "Course completion status" "block"
+
+  Scenario: Add the block to a the course where completion is not set
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Course completion status" block
+    Then I should see "No completion criteria set for this course" in the "Course completion status" "block"
+
+  Scenario: Add the block to a course with criteria and view as an untracked role.
+    Given the following "activities" exist:
+      | activity | course | idnumber | name           | intro                 |
+      | page     | C1     | page1    | Test page name | Test page description |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test page name"
+    And I navigate to "Edit settings" node in "Page module administration"
+    And I set the following fields to these values:
+      | Completion tracking | Show activity as complete when conditions are met |
+      | Require view | 1 |
+    And I press "Save and return to course"
+    When I add the "Course completion status" block
+    And I navigate to "Course completion" node in "Course administration"
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Test page name | 1 |
+    And I press "Save changes"
+    Then I should see "You are currently not being tracked by completion in this course" in the "Course completion status" "block"
diff --git a/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature b/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature
new file mode 100644 (file)
index 0000000..7d3285f
--- /dev/null
@@ -0,0 +1,73 @@
+@block @block_completionstatus
+Feature: Enable Block Completion in a course using activity completion
+  In order to view the completion block in a course
+  As a teacher
+  I can add completion block to a course and set up activity completion
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And the following "courses" exist:
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And the following "activities" exist:
+      | activity | course | idnumber | name           | intro                 |
+      | page     | C1     | page1    | Test page name | Test page description |
+
+  Scenario: Add the block to a the course and add course completion items
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test page name"
+    And I navigate to "Edit settings" node in "Page module administration"
+    And I set the following fields to these values:
+      | Completion tracking | Show activity as complete when conditions are met |
+      | Require view | 1 |
+    And I press "Save and return to course"
+    And I add the "Course completion status" block
+    And I navigate to "Course completion" node in "Course administration"
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Test page name | 1 |
+    And I press "Save changes"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Status: Not yet started" in the "Course completion status" "block"
+    And I should see "0 of 1" in the "Activity completion" "table_row"
+
+  Scenario: Add the block to a the course and add course completion items
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test page name"
+    And I navigate to "Edit settings" node in "Page module administration"
+    And I set the following fields to these values:
+      | Completion tracking | Show activity as complete when conditions are met |
+      | Require view | 1 |
+    And I press "Save and return to course"
+    And I add the "Course completion status" block
+    And I navigate to "Course completion" node in "Course administration"
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Test page name | 1 |
+    And I press "Save changes"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test page name"
+    And I follow "C1"
+    Then I should see "Status: Pending" in the "Course completion status" "block"
+    And I should see "0 of 1" in the "Activity completion" "table_row"
+    And I trigger cron
+    And I am on site homepage
+    And I follow "Course 1"
+    And I should see "1 of 1" in the "Activity completion" "table_row"
+    And I follow "More details"
+    And I should see "Yes" in the "Activity completion" "table_row"
diff --git a/blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature b/blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature
new file mode 100644 (file)
index 0000000..1aaed0a
--- /dev/null
@@ -0,0 +1,100 @@
+@block @block_completionstatus
+Feature: Enable Block Completion in a course using manual completion by others
+  In order to view the completion block in a course
+  As a teacher
+  I can add completion block to a course and set up manual completion by others
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | teacher2 | Teacher | 2 | teacher1@example.com | T2 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And the following "courses" exist:
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | teacher2 | C1     | teacher        |
+      | student1 | C1     | student        |
+
+  Scenario: Add the block to a the course and mark a student complete.
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Course completion status" block
+    And I navigate to "Course completion" node in "Course administration"
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Teacher | 1 |
+    And I press "Save changes"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I should see "Status: Not yet started" in the "Course completion status" "block"
+    And I should see "No" in the "Teacher" "table_row"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Course completion" node in "Reports"
+    And I follow "Click to mark user complete"
+    And I trigger cron
+    And I am on site homepage
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Status: Complete" in the "Course completion status" "block"
+    And I should see "Yes" in the "Teacher" "table_row"
+    And I follow "More details"
+    And I should see "Yes" in the "Marked complete by Teacher" "table_row"
+
+
+  Scenario: Add the block to a the course and require multiple roles to mark a student complete.
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Course completion status" block
+    And I navigate to "Course completion" node in "Course administration"
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Teacher             | 1 |
+      | Non-editing teacher | 1 |
+      | id_role_aggregation | ALL selected roles to mark when the condition is met |
+    And I press "Save changes"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I should see "Status: Not yet started" in the "Course completion status" "block"
+    And I should see "No" in the "Teacher" "table_row"
+    And I should see "No" in the "Non-editing teacher" "table_row"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Course completion" node in "Reports"
+    And I follow "Click to mark user complete"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I should see "Status: In progress" in the "Course completion status" "block"
+    And I should see "Yes" in the "Teacher" "table_row"
+    And I should see "No" in the "Non-editing teacher" "table_row"
+    And I follow "More details"
+    And I should see "Yes" in the "Marked complete by Teacher" "table_row"
+    And I should see "No" in the "Marked complete by Non-editing teacher" "table_row"
+    And I log out
+    And I log in as "teacher2"
+    And I follow "Course 1"
+    And I navigate to "Course completion" node in "Reports"
+    And I follow "Click to mark user complete"
+    And I trigger cron
+    And I am on site homepage
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Status: Complete" in the "Course completion status" "block"
+    And I should see "Yes" in the "Teacher" "table_row"
+    And I should see "Yes" in the "Non-editing teacher" "table_row"
+    And I follow "More details"
+    And I should see "Yes" in the "Marked complete by Teacher" "table_row"
+    And I should see "Yes" in the "Marked complete by Non-editing teacher" "table_row"
diff --git a/blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature b/blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature
new file mode 100644 (file)
index 0000000..8441d37
--- /dev/null
@@ -0,0 +1,44 @@
+@block @block_completionstatus @block_selfcompletion
+Feature: Enable Block Completion in a course using manual self completion
+  In order to view the completion block in a course
+  As a teacher
+  I can add completion block to a course and set up manual self completion
+
+  Scenario: Add the block to a the course and manually complete the course
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And the following "courses" exist:
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Course completion status" block
+    And I add the "Self completion" block
+    And I navigate to "Course completion" node in "Course administration"
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | id_criteria_self | 1 |
+    And I press "Save changes"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I should see "Status: Not yet started" in the "Course completion status" "block"
+    And I should see "No" in the "Self completion" "table_row"
+    And I follow "Complete course"
+    And I should see "Confirm self completion"
+    And I press "Yes"
+    And I should see "Status: In progress" in the "Course completion status" "block"
+    And I trigger cron
+    And I am on site homepage
+    And I follow "Course 1"
+    Then I should see "Status: Complete" in the "Course completion status" "block"
+    And I should see "Yes" in the "Self completion" "table_row"
+    And I follow "More details"
+    And I should see "Yes" in the "Self completion" "table_row"
diff --git a/blocks/course_list/tests/behat/block_course_list_category.feature b/blocks/course_list/tests/behat/block_course_list_category.feature
new file mode 100644 (file)
index 0000000..4f3fa50
--- /dev/null
@@ -0,0 +1,78 @@
+@block @block_course_list
+Feature: Enable the course_list block on a category page and view it's contents
+  In order to enable the course list block on a category page
+  As an admin
+  I can add the course list block to a category page
+
+  Background:
+    Given the following "categories" exist:
+      | name        | category | idnumber |
+      | Category 1  | 0        | CAT1     |
+      | Category 2  | 0        | CAT2     |
+      | Category 3  | CAT2     | CAT3     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | CAT1     |
+      | Course 3 | C3        | CAT2     |
+      | Course 4 | C4        | CAT3     |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | First    | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role           |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
+      | teacher1 | C3 | editingteacher |
+
+  Scenario: Add the course list block on category page and navigate to the course listing
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I follow "Course 1"
+    And I follow "Miscellaneous"
+    And I add the "Courses" block
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Miscellaneous"
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
+
+  Scenario: Add the course list block on category page and navigate to another course
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I follow "Course 1"
+    And I follow "Miscellaneous"
+    And I add the "Courses" block
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Miscellaneous"
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "Course 3"
+    And I should see "Course 3"
+
+  Scenario: Add the course list block on category page and view as an admin
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I follow "Course 1"
+    And I follow "Miscellaneous"
+    When I add the "Courses" block
+    Then I should see "Miscellaneous" in the "Course categories" "block"
+    And I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category 2" in the "Course categories" "block"
+    And I should not see "Category 3" in the "Course categories" "block"
+    And I should not see "Course 1" in the "Course categories" "block"
+    And I should not see "Course 2" in the "Course categories" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
diff --git a/blocks/course_list/tests/behat/block_course_list_course.feature b/blocks/course_list/tests/behat/block_course_list_course.feature
new file mode 100644 (file)
index 0000000..0441dfb
--- /dev/null
@@ -0,0 +1,94 @@
+@block @block_course_list
+Feature: Enable the course_list block on a course page and view it's contents
+  In order to enable the course list block on an course page
+  As a teacher
+  I can add the course list block to a course page
+
+  Background:
+    Given the following "categories" exist:
+      | name        | category | idnumber |
+      | Category 1  | 0        | CAT1     |
+      | Category 2  | 0        | CAT2     |
+      | Category 3  | CAT2     | CAT3     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | CAT1     |
+      | Course 3 | C3        | CAT2     |
+      | Course 4 | C4        | CAT3     |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | First    | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role           |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
+      | teacher1 | C3 | editingteacher |
+
+  Scenario: Add the course list block on course page and navigate to the course listing
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Courses" block
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
+
+  Scenario: Add the course list block on course page and navigate to another course
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Courses" block
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "Course 3"
+    And I should see "Course 3"
+
+  Scenario: Add the course list block on course page and view as an admin
+    Given I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Courses" block
+    Then I should see "Miscellaneous" in the "Course categories" "block"
+    And I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category 2" in the "Course categories" "block"
+    And I should not see "Category 3" in the "Course categories" "block"
+    And I should not see "Course 1" in the "Course categories" "block"
+    And I should not see "Course 2" in the "Course categories" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
+
+  Scenario: View the course list block on course page with hide all courses link enabled
+    Given the following config values are set as admin:
+      | block_course_list_hideallcourseslink | 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Courses" block
+    Then I should not see "All courses" in the "My courses" "block"
+
+  Scenario: View the course list block on course page with admin sees own course enabled
+    Given the following config values are set as admin:
+      | block_course_list_adminview | own |
+    And the following "course enrolments" exist:
+      | user | course | role           |
+      | admin | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Courses" block
+    Then I should not see "Miscellaneous" in the "My courses" "block"
+    And I should not see "Category 1" in the "My courses" "block"
+    And I should not see "Category 2" in the "My courses" "block"
+    And I should not see "Category 3" in the "My courses" "block"
+    And I should see "Course 1" in the "My courses" "block"
+    And I should not see "Course 2" in the "My courses" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
diff --git a/blocks/course_list/tests/behat/block_course_list_dashboard.feature b/blocks/course_list/tests/behat/block_course_list_dashboard.feature
new file mode 100644 (file)
index 0000000..33f5059
--- /dev/null
@@ -0,0 +1,61 @@
+@block @block_course_list
+Feature: Enable the course_list block on the dashboard and view it's contents
+  In order to enable the course list block on the dashboard
+  As a user
+  I can add the course list block to the dashboard
+
+  Background:
+    Given the following "categories" exist:
+      | name        | category | idnumber |
+      | Category 1  | 0        | CAT1     |
+      | Category 2  | 0        | CAT2     |
+      | Category 3  | CAT2     | CAT3     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | CAT1     |
+      | Course 3 | C3        | CAT2     |
+      | Course 4 | C4        | CAT3     |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | First    | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role           |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
+      | teacher1 | C3 | editingteacher |
+
+  Scenario: Add the course list block on the dashboard and navigate to the course listing
+    Given I log in as "teacher1"
+    And I press "Customise this page"
+    When I add the "Courses" block
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
+
+  Scenario: Add the course list block on the dashboard and navigate to another course
+    Given I log in as "teacher1"
+    And I press "Customise this page"
+    When I add the "Courses" block
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "Course 3"
+    And I should see "Course 3"
+
+  Scenario: Add the course list block on the dashboard and view as an admin
+    Given I log in as "admin"
+    And I press "Customise this page"
+    When I add the "Courses" block
+    Then I should see "Miscellaneous" in the "Course categories" "block"
+    And I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category 2" in the "Course categories" "block"
+    And I should not see "Category 3" in the "Course categories" "block"
+    And I should not see "Course 1" in the "Course categories" "block"
+    And I should not see "Course 2" in the "Course categories" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
diff --git a/blocks/course_list/tests/behat/block_course_list_frontpage.feature b/blocks/course_list/tests/behat/block_course_list_frontpage.feature
new file mode 100644 (file)
index 0000000..e13a380
--- /dev/null
@@ -0,0 +1,86 @@
+@block @block_course_list
+Feature: Enable the course_list block on the frontpage and view it's contents
+  In order to enable the course list block on the frontpage
+  As an admin
+  I can add the course list block to the frontpage
+
+  Background:
+    Given the following "categories" exist:
+      | name        | category | idnumber |
+      | Category 1  | 0        | CAT1     |
+      | Category 2  | 0        | CAT2     |
+      | Category 3  | CAT2     | CAT3     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | CAT1     |
+      | Course 3 | C3        | CAT2     |
+      | Course 4 | C4        | CAT3     |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | First    | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role           |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
+      | teacher1 | C3 | editingteacher |
+
+  Scenario: Add the course list block on the frontpage and navigate to the course listing
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Courses" block
+    And I log out
+    When I log in as "teacher1"
+    And I am on site homepage
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
+
+  Scenario: Add the course list block on the frontpage page and navigate to another course
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Courses" block
+    And I log out
+    When I log in as "teacher1"
+    And I am on site homepage
+    Then I should see "Course 1" in the "My courses" "block"
+    And I should see "Course 2" in the "My courses" "block"
+    And I should see "Course 3" in the "My courses" "block"
+    And I should not see "Course 4" in the "My courses" "block"
+    And I follow "Course 3"
+    And I should see "Course 3"
+
+  Scenario: Add the course list block on the frontpage page and view as an admin
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    When I add the "Courses" block
+    Then I should see "Miscellaneous" in the "Course categories" "block"
+    And I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category 2" in the "Course categories" "block"
+    And I should not see "Category 3" in the "Course categories" "block"
+    And I should not see "Course 1" in the "Course categories" "block"
+    And I should not see "Course 2" in the "Course categories" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
+
+  Scenario: Add the course list block on the frontpage page and view as a guest
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Courses" block
+    And I log out
+    When I log in as "guest"
+    Then I should see "Miscellaneous" in the "Course categories" "block"
+    And I should see "Category 1" in the "Course categories" "block"
+    And I should see "Category 2" in the "Course categories" "block"
+    And I should not see "Category 3" in the "Course categories" "block"
+    And I should not see "Course 1" in the "Course categories" "block"
+    And I should not see "Course 2" in the "Course categories" "block"
+    And I follow "All courses"
+    And I should see "Miscellaneous"
diff --git a/blocks/course_overview/tests/behat/block_course_overview.feature b/blocks/course_overview/tests/behat/block_course_overview.feature
new file mode 100644 (file)
index 0000000..2758c82
--- /dev/null
@@ -0,0 +1,162 @@
+@block @block_course_overview
+Feature: View the course overview block on the dashboard and test it's functionality
+  In order to view the course overview block on the dashboard
+  As an admin
+  I can configure the course overview block
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And the following "categories" exist:
+      | name        | category | idnumber |
+      | Category 1  | 0        | CAT1     |
+      | Category 2  | CAT1     | CAT2     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | CAT1     |
+      | Course 3 | C3        | CAT2     |
+
+  Scenario: View the block by a user without any enrolments
+    Given I log in as "student1"
+    Then I should see "No course information to show" in the "Course overview" "block"
+
+  Scenario: View the block by a user with several enrolments
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+    When I log in as "student1"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+
+  Scenario: View the block by a user with several enrolments and limit the number of courses.
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+    When I log in as "student1"
+    And I press "Customise this page"
+    And I select "1" from the "Number of courses to display:" singleselect
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should see "You have 2 hidden courses"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I follow "Show all courses"
+    And I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+
+  Scenario: View the block by a user with several enrolments and an admin set default max courses.
+    Given the following config values are set as admin:
+      | defaultmaxcourses | 2 | block_course_overview |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+    When I log in as "student1"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "You have 1 hidden course"
+    And I press "Customise this page"
+    And I select "Always show all" from the "Number of courses to display:" singleselect
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should not see "You have 1 hidden course"
+
+  Scenario: View the block by a user with several enrolments and an admin enforced maximum displayed courses.
+    Given the following config values are set as admin:
+      | defaultmaxcourses      | 2 | block_course_overview |
+      | forcedefaultmaxcourses | 1 | block_course_overview |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+    When I log in as "student1"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "You have 1 hidden course"
+    And I press "Customise this page"
+    And I should not see "Always show all"
+
+  Scenario: View the block by a user with the welcome area enabled and messaging disabled.
+    Given the following config values are set as admin:
+      | showwelcomearea | 1 | block_course_overview |
+      | messaging       | 0 |                       |
+    When I log in as "student1"
+    Then I should see "Welcome Student" in the "Course overview" "block"
+    And I should not see "messages" in the "Course overview" "block"
+
+  Scenario: View the block by a user with both the welcome area and messaging enabled.
+    Given the following config values are set as admin:
+      | showwelcomearea | 1 | block_course_overview |
+    When I log in as "student1"
+    Then I should see "Welcome Student" in the "Course overview" "block"
+    And I should see "You have no unread messages" in the "Course overview" "block"
+    And I follow "messages"
+    And I should see "Contact list empty"
+
+  Scenario: View the block by a user with the welcome area and the user having messages.
+    Given the following config values are set as admin:
+      | showwelcomearea | 1 | block_course_overview |
+    And I log in as "student1"
+    And I should see "Welcome Student" in the "Course overview" "block"
+    And I should see "You have no unread messages" in the "Course overview" "block"
+    And I follow "messages"
+    And I send "This is message 1" message to "Teacher 1" user
+    And I send "This is message 2" message to "Teacher 1" user
+    When I log out
+    And I log in as "teacher1"
+    Then I should see "Welcome Teacher" in the "Course overview" "block"
+    And I should see "You have 2 unread messages" in the "Course overview" "block"
+
+  Scenario: View the block by a user with the parent categories displayed.
+    Given the following config values are set as admin:
+      | showcategories | Parent category only | block_course_overview |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+    When I log in as "student1"
+    Then I should see "Miscellaneous" in the "Course overview" "block"
+    And I should see "Category 1" in the "Course overview" "block"
+    And I should see "Category 2" in the "Course overview" "block"
+    And I should not see "Category 1 / Category 1" in the "Course overview" "block"
+
+  Scenario: View the block by a user with the full categories displayed.
+    Given the following config values are set as admin:
+      | showcategories | 2 | block_course_overview |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+    When I log in as "student1"
+    Then I should see "Miscellaneous" in the "Course overview" "block"
+    And I should see "Category 1 / Category 2" in the "Course overview" "block"
+
+  @javascript
+  Scenario: View the block by a user with the show children option enabled.
+    Given the following config values are set as admin:
+      | showchildren | 1 | block_course_overview |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments"
+    And I click on "Enable" "link" in the "Course meta link" "table_row"
+    And I am on site homepage
+    And I follow "Course 2"
+    And I navigate to "Enrolment methods" node in "Users"
+    And I add "Course meta link" enrolment method with:
+      | Link course | C1 |
+    And I log out
+    When I log in as "student1"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Includes C1" in the "Course overview" "block"
index 43e8378..9038d47 100644 (file)
@@ -39,7 +39,7 @@
     <tbody>
         {{#competencies}}
         <tr>
-            <td><a href="{{usercompetency.url}}">{{competency.shortname}}</a></td>
+            <td><a href="{{usercompetency.url}}">{{{competency.shortname}}}</a></td>
             <td>{{user.fullname}}</td>
             <td>{{usercompetency.statusname}}</td>
         </tr>
index 9267a35..124dcd3 100644 (file)
@@ -39,7 +39,7 @@
     <tbody>
         {{#plans}}
         <tr>
-            <td><a href="{{plan.url}}">{{plan.name}}</a></td>
+            <td><a href="{{plan.url}}">{{{plan.name}}}</a></td>
             <td>{{user.fullname}}</td>
             <td>{{plan.statusname}}</td>
         </tr>
index cc25143..8ad8aa4 100644 (file)
@@ -42,7 +42,7 @@
             {{#hasactiveplans}}
                 <ul>
                     {{#activeplans}}
-                        <li><a href="{{url}}">{{name}}</a></li>
+                        <li><a href="{{url}}">{{{name}}}</a></li>
                     {{/activeplans}}
                     {{#hasmoreplans}}
                         <li class="more"><a href="{{plansurl}}">{{#str}}viewmore, block_lp{{/str}}</a></li>
@@ -60,7 +60,7 @@
             <ul>
                 {{#compstoreview}}
                     <li>
-                        <a href="{{usercompetency.url}}">{{competency.shortname}}</a> ({{user.fullname}}) - {{usercompetency.statusname}}
+                        <a href="{{usercompetency.url}}">{{{competency.shortname}}}</a> ({{user.fullname}}) - {{usercompetency.statusname}}
                     </li>
                 {{/compstoreview}}
                 {{#hasmorecompstoreview}}
@@ -75,7 +75,7 @@
             <ul>
                 {{#planstoreview}}
                     <li>
-                        <a href="{{plan.url}}">{{plan.name}}</a> ({{user.fullname}}) - {{plan.statusname}}
+                        <a href="{{plan.url}}">{{{plan.name}}}</a> ({{user.fullname}}) - {{plan.statusname}}
                     </li>
                 {{/planstoreview}}
                 {{#hasmoreplanstoreview}}
index b7cfef8..6d80a9f 100644 (file)
Binary files a/blocks/navigation/amd/build/ajax_response_renderer.min.js and b/blocks/navigation/amd/build/ajax_response_renderer.min.js differ
index 33f508c..5246ecf 100644 (file)
@@ -44,6 +44,7 @@ define(['jquery'], function($) {
     function buildDOM(rootElement, nodes) {
         var ul = $('<ul></ul>');
         ul.attr('role', 'group');
+        ul.attr('aria-hidden', true);
 
         $.each(nodes, function(index, node) {
             if (typeof node !== 'object') {
@@ -52,23 +53,26 @@ define(['jquery'], function($) {
 
             var li = $('<li></li>');
             var p = $('<p></p>');
+            var id = node.id || node.key + '_tree_item';
             var icon = null;
             var isBranch = (node.expandable || node.haschildren) ? true : false;
 
             p.addClass('tree_item');
-            p.attr('id', node.id);
-            li.attr('role', 'treeitem');
+            p.attr('id', id);
+            p.attr('role', 'treeitem');
+            // Negative tab index to allow it to receive focus.
+            p.attr('tabindex', '-1');
 
             if (node.requiresajaxloading) {
-                li.attr('data-requires-ajax', true);
-                li.attr('data-node-id', node.id);
-                li.attr('data-node-key', node.key);
-                li.attr('data-node-type', node.type);
+                p.attr('data-requires-ajax', true);
+                p.attr('data-node-id', node.id);
+                p.attr('data-node-key', node.key);
+                p.attr('data-node-type', node.type);
             }
 
             if (isBranch) {
                 li.addClass('collapsed contains_branch');
-                li.attr('aria-expanded', false);
+                p.attr('aria-expanded', false);
                 p.addClass('branch');
             }
 
@@ -121,14 +125,18 @@ define(['jquery'], function($) {
             ul.append(li);
 
             if (node.children && node.children.length) {
-                buildDOM(li, node.children);
+                buildDOM(p, node.children);
             } else if (isBranch && !node.requiresajaxloading) {
                 li.removeClass('contains_branch');
-                li.addClass('emptybranch');
+                p.addClass('emptybranch');
             }
         });
 
-        rootElement.append(ul);
+        rootElement.parent().append(ul);
+        var id = rootElement.attr('id') + '_group';
+        ul.attr('id', id);
+        rootElement.attr('aria-owns', id);
+        rootElement.attr('role', 'treeitem');
     }
 
     return {
@@ -136,9 +144,16 @@ define(['jquery'], function($) {
             // The first element of the response is the existing node so we start with processing the children.
             if (nodes.children && nodes.children.length) {
                 buildDOM(element, nodes.children);
+
+                var item = element.children("[role='treeitem']").first();
+                var group = element.find('#' + item.attr('aria-owns'));
+
+                item.attr('aria-expanded', true);
+                group.attr('aria-hidden', false);
             } else {
-                if (element.hasClass('contains_branch')) {
-                    element.removeClass('contains_branch').addClass('emptybranch');
+                if (element.parent().hasClass('contains_branch')) {
+                    element.parent().removeClass('contains_branch');
+                    element.addClass('emptybranch');
                 }
             }
         }
index 86565e8..8ead8fe 100644 (file)
@@ -32,6 +32,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class block_navigation_renderer extends plugin_renderer_base {
+
     /**
      * Returns the content of the navigation tree.
      *
@@ -77,33 +78,37 @@ class block_navigation_renderer extends plugin_renderer_base {
             if (!$item->display && !$item->contains_active_node()) {
                 continue;
             }
-            $content = $item->get_content();
-            $title = $item->get_title();
 
             $isexpandable = (empty($expansionlimit) || ($item->type > navigation_node::TYPE_ACTIVITY || $item->type < $expansionlimit) || ($item->contains_active_node() && $item->children->count() > 0));
-            $isbranch = $isexpandable && ($item->children->count() > 0 || ($item->has_children() && (isloggedin() || $item->type <= navigation_node::TYPE_CATEGORY)));
 
             // Skip elements which have no content and no action - no point in showing them
             if (!$isexpandable && empty($item->action)) {
                 continue;
             }
 
+            $id = $item->id ? $item->id : uniqid();
+            $content = $item->get_content();
+            $title = $item->get_title();
+            $ulattr = ['id' => $id . '_group', 'role' => 'group'];
+            $liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth]];
+            $pattr = ['class' => ['tree_item'], 'role' => 'treeitem'];
+            $pattr += !empty($item->id) ? ['id' => $item->id] : [];
+            $isbranch = $isexpandable && ($item->children->count() > 0 || ($item->has_children() && (isloggedin() || $item->type <= navigation_node::TYPE_CATEGORY)));
             $hasicon = ((!$isbranch || $item->type == navigation_node::TYPE_ACTIVITY || $item->type == navigation_node::TYPE_RESOURCE) && $item->icon instanceof renderable);
+            $icon = '';
 
             if ($hasicon) {
+                $liattr['class'][] = 'item_with_icon';
+                $pattr['class'][] = 'hasicon';
                 $icon = $this->output->render($item->icon);
                 // Because an icon is being used we're going to wrap the actual content in a span.
                 // This will allow designers to create columns for the content, as we've done in styles.css.
                 $content = $icon . html_writer::span($content, 'item-content-wrap');
-            } else {
-                $icon = '';
             }
-
             if ($item->helpbutton !== null) {
                 $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton'));
             }
-
-            if ($content === '') {
+            if (empty($content)) {
                 continue;
             }
 
@@ -129,70 +134,47 @@ class block_navigation_renderer extends plugin_renderer_base {
                 $content = html_writer::link($item->action, $content, $attributes);
             }
 
-            // This applies to the li item which contains all child lists too.
-            $liclasses = array($item->get_css_type(), 'depth_'.$depth);
-
-            // Class attribute on the div item which only contains the item content.
-            $divclasses = array('tree_item');
-
-            $liexpandable = array();
-            $lirole = array('role' => 'treeitem');
             if ($isbranch) {
-                $liclasses[] = 'contains_branch';
-                if ($depth == 1) {
-                    $liexpandable = array(
-                        'data-expandable' => 'false',
-                        'data-collapsible' => 'false'
-                    );
-                } else {
-                    $liexpandable = array(
-                        'aria-expanded' => ($item->has_children() &&
-                            (!$item->forceopen || $item->collapse)) ? "false" : "true");
-                }
-
+                $pattr['class'][] = 'branch';
+                $liattr['class'][] = 'contains_branch';
+                $pattr += ['aria-expanded' => ($item->has_children() && (!$item->forceopen || $item->collapse)) ? "false" : "true"];
                 if ($item->requiresajaxloading) {
-                    $liexpandable['data-requires-ajax'] = 'true';
-                    $liexpandable['data-loaded'] = 'false';
-                    $liexpandable['data-node-id'] = $item->id;
-                    $liexpandable['data-node-key'] = $item->key;
-                    $liexpandable['data-node-type'] = $item->type;
+                    $pattr += [
+                        'data-requires-ajax' => 'true',
+                        'data-loaded' => 'false',
+                        'data-node-id' => $item->id,
+                        'data-node-key' => $item->key,
+                        'data-node-type' => $item->type
+                    ];
+                } else {
+                    $pattr += ['aria-owns' => $id . '_group'];
                 }
-
-                $divclasses[] = 'branch';
-            } else {
-                $divclasses[] = 'leaf';
-            }
-            if ($hasicon) {
-                // Add this class if the item has an icon, whether it is a branch or not.
-                $liclasses[] = 'item_with_icon';
-                $divclasses[] = 'hasicon';
             }
+
             if ($item->isactive === true) {
-                $liclasses[] = 'current_branch';
+                $liattr['class'][] = 'current_branch';
             }
             if (!empty($item->classes) && count($item->classes)>0) {
-                $divclasses[] = join(' ', $item->classes);
+                $pattr['class'] = array_merge($pattr['class'], $item->classes);
             }
 
-            // Now build attribute arrays.
-            $liattr = array('class' => join(' ', $liclasses)) + $liexpandable + $lirole;
-            $divattr = array('class'=>join(' ', $divclasses));
-            if (!empty($item->id)) {
-                $divattr['id'] = $item->id;
+            $liattr['class'] = join(' ', $liattr['class']);
+            $pattr['class'] = join(' ', $pattr['class']);
+
+            $pattr += $depth == 1 ? ['data-collapsible' => 'false'] : [];
+            if (isset($pattr['aria-expanded']) && $pattr['aria-expanded'] === 'false') {
+                $ulattr += ['aria-hidden' => 'true'];
             }
 
             // Create the structure.
-            $content = html_writer::tag('p', $content, $divattr);
+            $content = html_writer::tag('p', $content, $pattr);
             if ($isexpandable) {
-                $content .= $this->navigation_node($item->children, array('role' => 'group'), $expansionlimit,
-                    $options, $depth + 1);
+                $content .= $this->navigation_node($item->children, $ulattr, $expansionlimit, $options, $depth + 1);
             }
             if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
                 $content = html_writer::empty_tag('hr') . $content;
             }
-            if ($depth == 1) {
-                $liattr['tabindex'] = '0';
-            }
+
             $liattr['aria-labelledby'] = $nodetextid;
             $content = html_writer::tag('li', $content, $liattr);
             $lis[] = $content;
@@ -207,5 +189,4 @@ class block_navigation_renderer extends plugin_renderer_base {
         // The source is complex already anyway.
         return html_writer::tag('ul', implode('', $lis), $attrs);
     }
-
 }
index c8aa881..47ef1c2 100644 (file)
-.block_navigation .block_tree {
-    margin: 0;
-    list-style: none;
-}
-
-.block_navigation .block_tree .depth_1 > .tree_item.branch,
-.block_navigation .block_tree .depth_1 > .tree_item.emptybranch {
-    padding-left: 0;
-    background-image: none;
-}
-
-.block_navigation .block_tree .tree_item {
-    margin: 3px 0;
-    background-position: 0 50%;
-    background-repeat: no-repeat;
-}
-
-.block_navigation .block_tree .tree_item.branch {
-    padding-left: 21px;
-    cursor: pointer;
-    background-image: url('[[pix:t/expanded]]');
-}
-
-.block_navigation .block_tree .tree_item.emptybranch {
-    padding-left: 21px;
-    background-image: url('[[pix:t/collapsed_empty]]');
-}
-
-.block_navigation .block_tree .tree_item.loadingbranch {
-    background-image: url('[[pix:i/loading_small]]');
-}
-
-.block_navigation .block_tree .loading .tree_item.branch {
-    background-image: url('[[pix:i/loading_small]]');
-}
-
-.block_navigation .block_tree .emptybranch .tree_item,
-.block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
-    padding-left: 21px;
-    background-image: url('[[pix:t/collapsed_empty]]');
-}
-
-.block_navigation .block_tree .tree_item img {
-    width: 16px;
-    height: 16px;
-    margin-top: 3px;
-    margin-right: 5px;
-    vertical-align: top;
-}
-
-.block_navigation .block_tree .tree_item.active_tree_node {
-    font-weight: bold;
-}
-
-.block_navigation .block_tree .tree_item.hasicon {
-    white-space: nowrap;
-}
-
-.block_navigation .block_tree .tree_item.hasicon .item-content-wrap {
-    display: inline-block;
-    white-space: normal;
-    width: calc(100% - 21px);
-}
-
-.block_navigation .block_tree ul {
-    margin: 0;
-}
-
-.block_navigation .block_tree ul ul {
-    margin: 0 0 0 16px;
-    list-style: none;
-}
-
-.jsenabled .block_navigation .block_tree [aria-expanded="false"] ul {
-    display: none;
-}
-
-.jsenabled .block_navigation .block_tree [aria-expanded="false"] .tree_item.branch {
-    background-image: url('[[pix:t/collapsed]]');
-}
-
-.jsenabled .block_navigation .block_tree [aria-expanded="false"].loading .tree_item.branch {
-    background-image: url('[[pix:i/loading_small]]');
-}
-
-.jsenabled .block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
-    padding-left: 21px;
-    background-image: url('[[pix:t/collapsed_empty]]');
-}
-
-.jsenabled .block_navigation.dock_on_load {
-    display: none;
-}
-
-.dir-rtl .block_navigation .block_tree .depth_1 .tree_item {
-    padding-left: 0;
-}
-
-.dir-rtl .block_navigation .block_tree .tree_item {
-    background-position: 100% 50%;
-}
-
-.dir-rtl .block_navigation .block_tree .tree_item.branch {
-    padding-right: 21px;
-    padding-left: 0;
-}
-
-.dir-rtl .block_navigation .block_tree .tree_item.emptybranch,
-.dir-rtl .block_navigation .block_tree .emptybranch .tree_item {
-    padding-right: 21px;
-    padding-left: 0;
-    background-image: url('[[pix:t/collapsed_empty_rtl]]');
-}
-
-.dir-rtl .block_navigation .block_tree .tree_item img {
-    margin-right: 0;
-    margin-left: 5px;
-}
-
-.dir-rtl .block_navigation .block_tree ul {
-    margin: 0 16px 0 0;
-}
-
-.dir-rtl.jsenabled .block_navigation .block_tree [aria-expanded="false"] .tree_item.branch {
-    background-image: url('[[pix:t/collapsed_rtl]]');
-}
-
-.dir-rtl.jsenabled .block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
-    background-image: url('[[pix:t/collapsed_empty_rtl]]');
-}
+.block_navigation .block_tree .depth_1 > .tree_item.branch {padding-left: 0; background-image:none;}
+.block_navigation .block_tree .depth_1 > ul {margin: 0;}
+.block_navigation .block_tree ul {margin-left: 18px;}
+.block_navigation .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
+.block_navigation .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+
+.block_navigation .block_tree .tree_item {cursor:pointer; padding-left: 0;margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
+.block_navigation .block_tree .tree_item.branch {padding-left: 21px;}
+.block_navigation .block_tree .active_tree_node {font-weight:bold;}
+
+.block_navigation .block_tree [aria-expanded="true"] {background-image: url('[[pix:t/expanded]]');}
+.block_navigation .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed]]');}
+.block_navigation .block_tree [aria-expanded="true"].emptybranch  {background-image: url('[[pix:t/collapsed_empty]]');}
+.block_navigation .block_tree [aria-expanded="false"].loading  {background-image: url('[[pix:i/loading_small]]');}
+.block_navigation .block_tree [aria-hidden="false"] {display: block;}
+.block_navigation .block_tree  [aria-hidden="true"] {display: none;}
+
+/** Internet explorer specific rules **/
+.ie6 .block_navigation .block_tree .tree_item {width:100%;}
+
+/** Overide for RTL layout **/
+.dir-rtl .block_navigation .block_tree p.hasicon {padding-left: 0px; padding-right: 21px;}
+.dir-rtl .block_navigation .block_tree .tree_item {background-position: 100% 50%;}
+.dir-rtl .block_navigation .block_tree .tree_item.branch {padding-right: 21px; padding-left: 0;}
+.dir-rtl .block_navigation .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed_rtl]]');}
+.dir-rtl .block_navigation .block_tree [aria-expanded="true"].emptybranch {background-image: url('[[pix:t/collapsed_empty_rtl]]');}
+.dir-rtl .block_navigation .block_tree [aria-expanded="false"].loading {background-image: url('[[pix:i/loading_small]]');}
+.dir-rtl .block_navigation .block_tree .tree_item img {margin-right: 0; margin-left: 5px;}
+.dir-rtl .block_navigation .block_tree ul {margin: 0 16px 0 0;}
index f8fcf89..26046cd 100644 (file)
@@ -47,19 +47,30 @@ class block_rss_client_edit_form extends block_edit_form {
             $mform->setDefault('config_shownumentries', 5);
         }
 
-        $rssfeeds = $DB->get_records_sql_menu('
-                SELECT id,
-                       CASE WHEN preferredtitle = ? THEN ' . $DB->sql_compare_text('title', 64) .' ELSE preferredtitle END
-                FROM {block_rss_client}
-                WHERE userid = ? OR shared = 1
-                ORDER BY CASE WHEN preferredtitle = ? THEN ' . $DB->sql_compare_text('title', 64) . ' ELSE preferredtitle END ',
-                array('', $USER->id, ''));
+        $insql = '';
+        $params = array('userid' => $USER->id);
+        $rssconfig = unserialize(base64_decode($this->block->instance->configdata));
+        if ($rssconfig && !empty($rssconfig->rssid)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($rssconfig->rssid, SQL_PARAMS_NAMED);
+            $insql = "OR id $insql ";
+            $params += $inparams;
+        }
+
+        $titlesql = "CASE WHEN preferredtitle = '' THEN {$DB->sql_compare_text('title', 64)} ELSE preferredtitle END";
+
+        $rssfeeds = $DB->get_records_sql_menu("
+                SELECT id, $titlesql
+                  FROM {block_rss_client}
+                 WHERE userid = :userid OR shared = 1 $insql
+                 ORDER BY $titlesql",
+                $params);
+
         if ($rssfeeds) {
             $select = $mform->addElement('select', 'config_rssid', get_string('choosefeedlabel', 'block_rss_client'), $rssfeeds);
             $select->setMultiple(true);
 
         } else {
-            $mform->addElement('static', 'config_rssid', get_string('choosefeedlabel', 'block_rss_client'),
+            $mform->addElement('static', 'config_rssid_no_feeds', get_string('choosefeedlabel', 'block_rss_client'),
                     get_string('nofeeds', 'block_rss_client'));
         }
 
index d5572e8..10afa75 100644 (file)
@@ -69,52 +69,61 @@ class block_settings_renderer extends plugin_renderer_base {
             }
 
             $isbranch = ($item->children->count()>0  || $item->nodetype==navigation_node::NODETYPE_BRANCH);
-            $hasicon = (!$isbranch && $item->icon instanceof renderable);
 
             if ($isbranch) {
                 $item->hideicon = true;
             }
+
             $content = $this->output->render($item);
+            $id = $item->id ? $item->id : uniqid();
+            $ulattr = ['id' => $id . '_group', 'role' => 'group'];
+            $liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'tabindex' => '-1'];
+            $pattr = ['class' => ['tree_item'], 'role' => 'treeitem'];
+            $pattr += !empty($item->id) ? ['id' => $item->id] : [];
+            $hasicon = (!$isbranch && $item->icon instanceof renderable);
 
-            // this applies to the li item which contains all child lists too
-            $liclasses = array($item->get_css_type());
-            $liexpandable = array();
             if ($isbranch) {
-                $liclasses[] = 'contains_branch';
+                $liattr['class'][] = 'contains_branch';
                 if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count() == 0
                         && $item->nodetype == navigation_node::NODETYPE_BRANCH)) {
-                    $liexpandable = array('aria-expanded' => 'false');
+                    $pattr += ['aria-expanded' => 'false'];
                 } else {
-                    $liexpandable = array('aria-expanded' => 'true');
+                    $pattr += ['aria-expanded' => 'true'];
                 }
                 if ($item->requiresajaxloading) {
-                    $liexpandable['data-requires-ajax'] = 'true';
-                    $liexpandable['data-loaded'] = 'false';
+                    $pattr['data-requires-ajax'] = 'true';
+                    $pattr['data-loaded'] = 'false';
+                } else {
+                    $pattr += ['aria-owns' => $id . '_group'];
                 }
-
             } else if ($hasicon) {
-                $liclasses[] = 'item_with_icon';
+                $liattr['class'][] = 'item_with_icon';
+                $pattr['class'][] = 'hasicon';
             }
             if ($item->isactive === true) {
-                $liclasses[] = 'current_branch';
+                $liattr['class'][] = 'current_branch';
+            }
+            if (!empty($item->classes) && count($item->classes) > 0) {
+                $pattr['class'] = array_merge($pattr['class'], $item->classes);
             }
             $nodetextid = 'label_' . $depth . '_' . $number;
-            $liattr = array('class' => join(' ', $liclasses), 'tabindex' => '-1', 'role' => 'treeitem') + $liexpandable;
+
             // class attribute on the div item which only contains the item content
-            $divclasses = array('tree_item');
+            $pattr['class'][] = 'tree_item';
             if ($isbranch) {
-                $divclasses[] = 'branch';
+                $pattr['class'][] = 'branch';
             } else {
-                $divclasses[] = 'leaf';
-            }
-            if (!empty($item->classes) && count($item->classes)>0) {
-                $divclasses[] = join(' ', $item->classes);
+                $pattr['class'][] = 'leaf';
             }
-            $divattr = array('class'=>join(' ', $divclasses));
-            if (!empty($item->id)) {
-                $divattr['id'] = $item->id;
+
+            $liattr['class'] = join(' ', $liattr['class']);
+            $pattr['class'] = join(' ', $pattr['class']);
+
+            if (isset($pattr['aria-expanded']) && $pattr['aria-expanded'] === 'false') {
+                $ulattr += ['aria-hidden' => 'true'];
             }
-            $content = html_writer::tag('p', $content, $divattr) . $this->navigation_node($item, array(), $depth + 1);
+
+            $content = html_writer::tag('p', $content, $pattr) . $this->navigation_node($item, $ulattr, $depth + 1);
             if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
                 $content = html_writer::empty_tag('hr') . $content;
             }
index 6cb127c..31b27ec 100644 (file)
@@ -1,43 +1,27 @@
-/** JavaScript state rules **/
-.jsenabled .block_settings.dock_on_load,
-.block_settings .block_tree_box .requiresjs {display:none;}
-.jsenabled .block_settings .block_tree_box .requiresjs {display:inline;}
+.block_settings .block_tree ul {margin-left: 18px;}
+.block_settings .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
+.block_settings .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
 
-/** General display rules **/
-.block_settings .block_tree {margin:5px;padding-left:0px;overflow:visible;}
-.block_settings .block_tree li {margin:0;list-style: none;}
-.block_settings .block_tree li ul {padding-left:18px;margin:0;}
-
-.block_settings .block_tree li.item_with_icon > p {position:relative;}
-.block_settings .block_tree li.item_with_icon > p img {vertical-align:middle;position:absolute;left:0;top:-1px; width: 16px; height: 16px;}
-
-.block_settings .block_tree .tree_item {padding-left: 21px;margin:3px 0px;text-align:left;}
-
-.block_settings .block_tree .tree_item.branch {background-image: url([[pix:t/expanded]]);background-position: 0 10%;background-repeat: no-repeat;}
-.block_settings .block_tree .loading .tree_item.branch {background-image: url('[[pix:i/loading_small]]');}
+.block_settings .block_tree .tree_item.branch {padding-left: 21px;}
+.block_settings .block_tree .tree_item {cursor:pointer; margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
 .block_settings .block_tree .active_tree_node {font-weight:bold;}
-.jsenabled .block_settings .block_tree .tree_item.branch {cursor:pointer;}
-.jsenabled .block_settings .block_tree .emptybranch .tree_item,
-.jsenabled .block_settings .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
-    background-image: url([[pix:t/collapsed_empty]]);background-position: 0 10%;background-repeat: no-repeat;
-}
-.jsenabled .block_settings .block_tree [aria-expanded="false"] ul {display: none;}
-.jsenabled .block_settings .block_tree [aria-expanded="false"] .tree_item.branch {background-image: url([[pix:t/collapsed]]);}
-.jsenabled .block_settings .block_tree [aria-expanded="false"].loading .tree_item.branch {background-image: url('[[pix:i/loading_small]]');}
+
+.block_settings .block_tree [aria-expanded="true"] {background-image: url('[[pix:t/expanded]]');}
+.block_settings .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed]]');}
+.block_settings .block_tree [aria-expanded="true"].emptybranch  {background-image: url('[[pix:t/collapsed_empty]]');}
+.block_settings .block_tree [aria-expanded="false"].loading  {background-image: url('[[pix:i/loading_small]]');}
+.block_settings .block_tree [aria-hidden="false"] {display: block;}
+.block_settings .block_tree  [aria-hidden="true"] {display: none;}
 
 /** Internet explorer specific rules **/
 .ie6 .block_settings .block_tree .tree_item {width:100%;}
 
 /** Overide for RTL layout **/
-.dir-rtl .block_settings .block_tree {padding-right:0px;}
-.dir-rtl .block_settings .block_tree li ul {padding-left:0;padding-right: 18px;}
-.dir-rtl .block_settings .block_tree .tree_item {padding-right: 21px; padding-left: 0; text-align:right;}
-.dir-rtl .block_settings .block_tree .tree_item.branch {background-position: center right;}
-.dir-rtl .block_settings .block_tree li.item_with_icon > p img { right: 0; left: auto;}
-.jsenabled .block_settings .block_tree .tree_item.branch.loadingbranch {background-image:url([[pix:i/loading_small]]);}
-
-.jsenabled.dir-rtl .block_settings .block_tree .emptybranch .tree_item,
-.jsenabled.dir-rtl .block_settings .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
-    background-image: url([[pix:t/collapsed_empty_rtl]]);background-position: center right;
-}
-.jsenabled.dir-rtl .block_settings .block_tree [aria-expanded="false"] .tree_item.branch {background-image: url([[pix:t/collapsed_rtl]]);}
+.dir-rtl .block_settings .block_tree p.hasicon {padding-left: 0px; padding-right: 21px;}
+.dir-rtl .block_settings .block_tree .tree_item {background-position: 100% 50%;}
+.dir-rtl .block_settings .block_tree .tree_item.branch {padding-right: 21px; padding-left: 0;}
+.dir-rtl .block_settings .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed_rtl]]');}
+.dir-rtl .block_settings .block_tree [aria-expanded="true"].emptybranch {background-image: url('[[pix:t/collapsed_empty_rtl]]');}
+.dir-rtl .block_settings .block_tree [aria-expanded="false"].loading {background-image: url('[[pix:i/loading_small]]');}
+.dir-rtl .block_settings .block_tree .tree_item img {margin-right: 0; margin-left: 5px;}
+.dir-rtl .block_settings .block_tree ul {margin: 0 16px 0 0;}
index 866047d..26bbf00 100644 (file)
@@ -75,7 +75,7 @@ class block_site_main_menu extends block_list {
                         $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
                     }
 
-                    $this->content->items[] = $indent . $content;
+                    $this->content->items[] = $indent . html_writer::div($content, 'main-menu-content');
                 }
             }
             return $this->content;
@@ -142,7 +142,7 @@ class block_site_main_menu extends block_list {
                     } else {
                         $content = html_writer::div($courserenderer->course_section_cm_name($mod), ' activity');
                     }
-                    $this->content->items[] = $indent. $content . $editbuttons;
+                    $this->content->items[] = $indent . html_writer::div($content . $editbuttons, 'main-menu-content');
                 }
             }
         }
index f437bae..6d43c31 100644 (file)
@@ -1,9 +1,13 @@
 .block_site_main_menu li { clear: both; }
-.block_site_main_menu li .column { width: 100%; }
+.block_site_main_menu.block.list_block .unlist > li > .column {
+    /* Made specific to win over .block.list_block .unlist > li > .column. */
+    width: 100%;
+    display: table;
+}
 .block_site_main_menu li .buttons { float: right; margin: 0; }
 .dir-rtl .block_site_main_menu li .buttons { float: left; }
 .block_site_main_menu li .buttons a img{ vertical-align: text-bottom;}
 .block_site_main_menu .footer { margin-top: 1em; }
 .block_site_main_menu .section_add_menus noscript div { display: inline;}
 .block_site_main_menu .mod-indent,
-.block_site_main_menu .activity { display: table-cell; }
+.block_site_main_menu .main-menu-content { display: table-cell; }
index da95699..2383693 100644 (file)
@@ -241,6 +241,9 @@ class core_calendar_external extends external_api {
 
         foreach ($eventlist as $eventid => $eventobj) {
             $event = (array) $eventobj;
+            // Description formatting.
+            $calendareventobj = new calendar_event($event);
+            list($event['description'], $event['format']) = $calendareventobj->format_external_text();
 
             if ($hassystemcap) {
                 // User can see everything, no further check is needed.
index 5126684..d93396d 100644 (file)
@@ -2706,6 +2706,36 @@ class calendar_event {
             return false;
         }
     }
+
+    /**
+     * Format the text using the external API.
+     * This function should we used when text formatting is required in external functions.
+     *
+     * @return array an array containing the text formatted and the text format
+     */
+    public function format_external_text() {
+
+        if ($this->editorcontext === null) {
+            // Switch on the event type to decide upon the appropriate context to use for this event.
+            $this->editorcontext = $this->properties->context;
+
+            if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
+                    && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
+                // We don't have a context here, do a normal format_text.
+                return array(format_text($this->properties->description, $this->properties->format), $this->properties->format);
+            }
+        }
+
+        // Work out the item id for the editor, if this is a repeated event then the files will be associated with the original.
+        if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
+            $itemid = $this->properties->repeatid;
+        } else {
+            $itemid = $this->properties->id;
+        }
+
+        return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id,
+                                    'calendar', 'event_description', $itemid);
+    }
 }
 
 /**
index 7619b2a..dc5041a 100644 (file)
@@ -277,8 +277,28 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
 
         // Let's create a few events.
         $siteevent = $this->create_calendar_event('site', $USER->id, 'site');
+
+        // This event will have description with an inline fake image.
+        $draftidfile = file_get_unused_draft_itemid();
+        $usercontext = context_course::instance($course->id);
+        $filerecord = array(
+            'contextid' => $usercontext->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $draftidfile,
+            'filepath'  => '/',
+            'filename'  => 'fakeimage.png',
+        );
+        $fs = get_file_storage();
+        $fs->create_file_from_string($filerecord, 'img contents');
+
         $record = new stdClass();
         $record->courseid = $course->id;
+        $record->description = array(
+            'format' => FORMAT_HTML,
+            'text' => 'Text with img <img src="@@PLUGINFILE@@/fakeimage.png">',
+            'itemid' => $draftidfile
+        );
         $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 2, time(), $record);
         $userevent = $this->create_calendar_event('user', $USER->id);
         $record = new stdClass();
@@ -300,6 +320,18 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(5, count($events['events']));
         $this->assertEquals(0, count($events['warnings']));
 
+        // Expect the same URL in the description of two different events (because they are repeated).
+        $coursecontext = context_course::instance($course->id);
+        $expectedurl = "webservice/pluginfile.php/$coursecontext->id/calendar/event_description/$courseevent->id/fakeimage.png";
+        $withdescription = 0;
+        foreach ($events['events'] as $event) {
+            if (!empty($event['description'])) {
+                $withdescription++;
+                $this->assertContains($expectedurl, $event['description']);
+            }
+        }
+        $this->assertEquals(2, $withdescription);
+
         // Let's play around with caps.
         $this->setUser($user);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
index a70ac33..f58a59a 100644 (file)
@@ -1332,8 +1332,8 @@ class api {
         }
 
         $capability = 'moodle/competency:usercompetencyreview';
-        $ucfields = user_competency::get_sql_fields('uc');
-        $compfields = competency::get_sql_fields('c');
+        $ucfields = user_competency::get_sql_fields('uc', 'uc_');
+        $compfields = competency::get_sql_fields('c', 'c_');
         $usercols = array('id') + get_user_fieldnames();
         $userfields = array();
         foreach ($usercols as $field) {
@@ -1376,8 +1376,8 @@ class api {
         $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
         foreach ($records as $record) {
             $objects = (object) array(
-                'usercompetency' => new user_competency(0, user_competency::extract_record($record)),
-                'competency' => new competency(0, competency::extract_record($record)),
+                'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
+                'competency' => new competency(0, competency::extract_record($record, 'c_')),
                 'user' => persistent::extract_record($record, 'usr_'),
             );
             $competencies[] = $objects;
@@ -2369,8 +2369,8 @@ class api {
             $userid = $USER->id;
         }
 
-        $planfields = plan::get_sql_fields('p');
-        $tplfields = template::get_sql_fields('t');
+        $planfields = plan::get_sql_fields('p', 'plan_');
+        $tplfields = template::get_sql_fields('t', 'tpl_');
         $usercols = array('id') + get_user_fieldnames();
         $userfields = array();
         foreach ($usercols as $field) {
@@ -2412,11 +2412,11 @@ class api {
         $plans = array();
         $records = $DB->get_recordset_sql($select . $sql, $params, $skip, $limit);
         foreach ($records as $record) {
-            $plan = new plan(0, plan::extract_record($record));
+            $plan = new plan(0, plan::extract_record($record, 'plan_'));
             $template = null;
 
             if ($plan->is_based_on_template()) {
-                $template = new template(0, template::extract_record($record));
+                $template = new template(0, template::extract_record($record, 'tpl_'));
             }
 
             $plans[] = (object) array(
index 05f193d..8af4927 100644 (file)
@@ -64,7 +64,7 @@ class competency extends persistent {
                 'type' => PARAM_TEXT
             ),
             'idnumber' => array(
-                'type' => PARAM_TEXT
+                'type' => PARAM_RAW
             ),
             'description' => array(
                 'default' => '',
index 5e51dcc..b1f323a 100644 (file)
@@ -87,7 +87,7 @@ class competency_framework extends persistent {
                 'type' => PARAM_TEXT
             ),
             'idnumber' => array(
-                'type' => PARAM_TEXT
+                'type' => PARAM_RAW
             ),
             'description' => array(
                 'type' => PARAM_RAW,
index d09b142..ab9bbbb 100644 (file)
@@ -69,30 +69,30 @@ class user_summary_exporter extends exporter {
     public static function define_properties() {
         return array(
             'id' => array(
-                'type' => PARAM_INT,
+                'type' => \core_user::get_property_type('id'),
             ),
             'email' => array(
-                'type' => PARAM_TEXT,
+                'type' => \core_user::get_property_type('email'),
                 'default' => ''
             ),
             'idnumber' => array(
-                'type' => PARAM_NOTAGS,
+                'type' => \core_user::get_property_type('idnumber'),
                 'default' => ''
             ),
             'phone1' => array(
-                'type' => PARAM_NOTAGS,
+                'type' => \core_user::get_property_type('phone1'),
                 'default' => ''
             ),
             'phone2' => array(
-                'type' => PARAM_NOTAGS,
+                'type' => \core_user::get_property_type('phone2'),
                 'default' => ''
             ),
             'department' => array(
-                'type' => PARAM_TEXT,
+                'type' => \core_user::get_property_type('department'),
                 'default' => ''
             ),
             'institution' => array(
-                'type' => PARAM_TEXT,
+                'type' => \core_user::get_property_type('institution'),
                 'default' => ''
             )
         );
@@ -101,10 +101,10 @@ class user_summary_exporter extends exporter {
     public static function define_other_properties() {
         return array(
             'fullname' => array(
-                'type' => PARAM_TEXT
+                'type' => PARAM_RAW
             ),
             'identity' => array(
-                'type' => PARAM_TEXT
+                'type' => PARAM_RAW
             ),
             'profileurl' => array(
                 'type' => PARAM_URL
index 7db1c48..fe94961 100644 (file)
@@ -785,6 +785,7 @@ abstract class persistent {
      * @return string The SQL fragment.
      */
     public static function get_sql_fields($alias, $prefix = null) {
+        global $CFG;
         $fields = array();
 
         if ($prefix === null) {
@@ -798,7 +799,14 @@ abstract class persistent {
         $properties = array('id' => $id) + $properties;
 
         foreach ($properties as $property => $definition) {
-            $fields[] = $alias . '.' . $property . ' AS ' . $prefix . $property;
+            $as = $prefix . $property;
+            $fields[] = $alias . '.' . $property . ' AS ' . $as;
+
+            // Warn developers that the query will not always work.
+            if ($CFG->debugdeveloper && strlen($as) > 30) {
+                throw new coding_exception("The alias '$as' for column '$alias.$property' exceeds 30 characters" .
+                    " and will therefore not work across all supported databases.");
+            }
         }
 
         return implode(', ', $fields);
index d45ec01..57a72ed 100644 (file)
@@ -71,7 +71,8 @@ class plan_competency extends persistent {
                   JOIN {' . self::TABLE . '} plancomp
                     ON plancomp.competencyid = comp.id
                  WHERE plancomp.planid = ?
-              ORDER BY plancomp.sortorder ASC';
+              ORDER BY plancomp.sortorder ASC,
+                       plancomp.id ASC';
         $params = array($planid);
 
         // TODO MDL-52229 Handle hidden competencies.
index 41019e2..62a1ca7 100644 (file)
@@ -202,7 +202,8 @@ class template_competency extends persistent {
                   JOIN {' . self::TABLE . '} tplcomp
                     ON tplcomp.competencyid = comp.id
                  WHERE tplcomp.templateid = ?
-              ORDER BY tplcomp.sortorder ASC';
+              ORDER BY tplcomp.sortorder ASC,
+                       tplcomp.id ASC';
         $params = array($templateid);
 
         $results = $DB->get_records_sql($sql, $params);
index f003d38..1cf1272 100644 (file)
@@ -508,7 +508,8 @@ class user_competency extends persistent {
             $sql = "competencyid $insql";
         }
 
-        return self::get_records_select("userid = :userid AND $sql", $params);
+        // Order by ID to prevent random ordering.
+        return self::get_records_select("userid = :userid AND $sql", $params, 'id ASC');
     }
 
     /**
index e731ca6..c139061 100644 (file)
@@ -227,7 +227,8 @@ class user_competency_course extends persistent {
             $sql = "competencyid $insql";
         }
 
-        return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params);
+        // Order by ID to prevent random ordering.
+        return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params, 'id ASC');
     }
 
     /**
index 81b7791..e493e5b 100644 (file)
@@ -189,7 +189,8 @@ class user_competency_plan extends persistent {
                     ON ucp.competencyid = c.id
                    AND ucp.userid = :userid
                  WHERE ucp.planid = :planid
-              ORDER BY ucp.sortorder ASC';
+              ORDER BY ucp.sortorder ASC,
+                       ucp.id ASC';
         $params = array('userid' => $userid, 'planid' => $planid);
 
         $results = $DB->get_recordset_sql($sql, $params);
@@ -258,7 +259,8 @@ class user_competency_plan extends persistent {
             $sql = "competencyid $insql";
         }
 
-        return static::get_records_select("userid = :userid AND planid = :planid AND $sql", $params);
+        // Order by ID to prevent random ordering.
+        return static::get_records_select("userid = :userid AND planid = :planid AND $sql", $params, 'id ASC');
     }
 
     /**
index d0f86eb..9e5c4d3 100644 (file)
@@ -2686,17 +2686,17 @@ class core_competency_api_testcase extends advanced_testcase {
         $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency');
         $user = $dg->create_user();
 
-        $dg->create_scale(array("id" => "1", "scale" => "value1, value2"));
-        $dg->create_scale(array("id" => "2", "scale" => "value3, value4, value5, value6"));
+        $s1 = $dg->create_scale(array("scale" => "value1, value2"));
+        $s2 = $dg->create_scale(array("scale" => "value3, value4, value5, value6"));
 
-        $scaleconfiguration1 = '[{"scaleid":"1"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
+        $scaleconfiguration1 = '[{"scaleid":"'.$s1->id.'"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]';
-        $scaleconfiguration2 = '[{"scaleid":"2"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},'
+        $scaleconfiguration2 = '[{"scaleid":"'.$s2->id.'"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},'
                 . '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]';
 
         // Create a framework with scale configuration1.
         $frm = array(
-            'scaleid' => 1,
+            'scaleid' => $s1->id,
             'scaleconfiguration' => $scaleconfiguration1
         );
         $framework = $lpg->create_framework($frm);
@@ -2704,7 +2704,7 @@ class core_competency_api_testcase extends advanced_testcase {
 
         // Create competency with its own scale configuration.
         $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(),
-                                            'scaleid' => 2,
+                                            'scaleid' => $s2->id,
                                             'scaleconfiguration' => $scaleconfiguration2
                                         ));
 
@@ -3961,7 +3961,6 @@ class core_competency_api_testcase extends advanced_testcase {
         $dg = $this->getDataGenerator();
         $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency');
 
-        $currenttime = time();
         $syscontext = context_system::instance();
 
         // Create users.
@@ -3979,10 +3978,8 @@ class core_competency_api_testcase extends advanced_testcase {
         $pc1 = $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id()));
         $pc2 = $lpg->create_plan_competency(array('planid' => $p2->get_id(), 'competencyid' => $c1->get_id()));
 
-        // Create user competency. Add user_evidence and associate it to the user competency.
+        // Create user competency and add an evidence.
         $uc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id()));
-        $ue = $lpg->create_user_evidence(array('userid' => $user->id));
-        $uec = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue->get_id(), 'competencyid' => $c1->get_id()));
         $e1 = $lpg->create_evidence(array('usercompetencyid' => $uc->get_id()));
 
         // Check both plans as one evidence.
@@ -3990,24 +3987,18 @@ class core_competency_api_testcase extends advanced_testcase {
         $this->assertEquals(1, count(api::list_evidence($user->id, $c1->get_id(), $p2->get_id())));
 
         // Complete second plan.
-        $currenttime += 1;
         $p2->set_status(plan::STATUS_COMPLETE);
         $p2->update();
-        $plansql = "UPDATE {" . plan::TABLE . "} SET timemodified = :currenttime WHERE id = :planid";
-        $DB->execute($plansql, array('currenttime' => $currenttime, 'planid' => $p2->get_id()));
 
-        // Add an other user evidence for the same competency.
-        $currenttime += 1;
-        $ue2 = $lpg->create_user_evidence(array('userid' => $user->id));
-        $uec2 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue2->get_id(), 'competencyid' => $c1->get_id()));
+        // Add another evidence for the same competency, but in the future (time + 1).
         $e2 = $lpg->create_evidence(array('usercompetencyid' => $uc->get_id()));
         $evidencesql = "UPDATE {" . evidence::TABLE . "} SET timecreated = :currenttime WHERE id = :evidenceid";
-        $DB->execute($evidencesql, array('currenttime' => $currenttime, 'evidenceid' => $e2->get_id()));
+        $DB->execute($evidencesql, array('currenttime' => time() + 1, 'evidenceid' => $e2->get_id()));
 
-        // Check first plan which is not completed as all evidences.
+        // Check that the first plan, which is not completed, has all the evidence.
         $this->assertEquals(2, count(api::list_evidence($user->id, $c1->get_id(), $p1->get_id())));
 
-        // Check second plan completed before the new evidence as only the first evidence.
+        // Check that the second plan, completed before the new evidence, only has the first piece of evidence.
         $listevidences = api::list_evidence($user->id, $c1->get_id(), $p2->get_id());
         $this->assertEquals(1, count($listevidences));
         $this->assertEquals($e1->get_id(), $listevidences[$e1->get_id()]->get_id());
@@ -4414,4 +4405,100 @@ class core_competency_api_testcase extends advanced_testcase {
 
         api::delete_evidence($ev1);
     }
+
+    public function test_list_plans_to_review() {
+        $dg = $this->getDataGenerator();
+        $this->resetAfterTest();
+        $ccg = $dg->get_plugin_generator('core_competency');
+        $sysctx = context_system::instance();
+        $this->setAdminUser();
+
+        $reviewer = $dg->create_user();
+        $roleallow = $dg->create_role();
+        $roleprohibit = $dg->create_role();
+        assign_capability('moodle/competency:planreview', CAP_ALLOW, $roleallow, $sysctx->id);
+        assign_capability('moodle/competency:planreview', CAP_PROHIBIT, $roleprohibit, $sysctx->id);
+        role_assign($roleallow, $reviewer->id, $sysctx->id);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $f1 = $ccg->create_framework();
+        $comp1 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]);
+        $p1a = $ccg->create_plan(['userid' => $u1->id, 'status' => plan::STATUS_WAITING_FOR_REVIEW]);
+        $p1b = $ccg->create_plan(['userid' => $u1->id, 'status' => plan::STATUS_IN_REVIEW, 'reviewerid' => $reviewer->id]);
+        $p1c = $ccg->create_plan(['userid' => $u1->id, 'status' => plan::STATUS_DRAFT]);
+        $p2a = $ccg->create_plan(['userid' => $u2->id, 'status' => plan::STATUS_WAITING_FOR_REVIEW]);
+        $p2b = $ccg->create_plan(['userid' => $u2->id, 'status' => plan::STATUS_IN_REVIEW]);
+        $p2c = $ccg->create_plan(['userid' => $u2->id, 'status' => plan::STATUS_ACTIVE]);
+        $p2d = $ccg->create_plan(['userid' => $u2->id, 'status' => plan::STATUS_ACTIVE]);
+        api::complete_plan($p2d);
+
+        // The reviewer can review all plans waiting for review, or in review where they are the reviewer.
+        $this->setUser($reviewer);
+        $result = api::list_plans_to_review();
+        $this->assertEquals(3, $result['count']);
+        $this->assertEquals($p1a->get_id(), $result['plans'][0]->plan->get_id());
+        $this->assertEquals($p1b->get_id(), $result['plans'][1]->plan->get_id());
+        $this->assertEquals($p2a->get_id(), $result['plans'][2]->plan->get_id());
+
+        // The reviewer cannot view the plans when they do not have the permission in the user's context.
+        role_assign($roleprohibit, $reviewer->id, context_user::instance($u2->id)->id);
+        accesslib_clear_all_caches_for_unit_testing();
+        $result = api::list_plans_to_review();
+        $this->assertEquals(2, $result['count']);
+        $this->assertEquals($p1a->get_id(), $result['plans'][0]->plan->get_id());
+        $this->assertEquals($p1b->get_id(), $result['plans'][1]->plan->get_id());
+    }
+
+    public function test_list_user_competencies_to_review() {
+        $dg = $this->getDataGenerator();
+        $this->resetAfterTest();
+        $ccg = $dg->get_plugin_generator('core_competency');
+        $sysctx = context_system::instance();
+        $this->setAdminUser();
+
+        $reviewer = $dg->create_user();
+        $roleallow = $dg->create_role();
+        $roleprohibit = $dg->create_role();
+        assign_capability('moodle/competency:usercompetencyreview', CAP_ALLOW, $roleallow, $sysctx->id);
+        assign_capability('moodle/competency:usercompetencyreview', CAP_PROHIBIT, $roleprohibit, $sysctx->id);
+        role_assign($roleallow, $reviewer->id, $sysctx->id);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $f1 = $ccg->create_framework();
+        $c1 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]);
+        $c2 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]);
+        $c3 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]);
+        $uc1a = $ccg->create_user_competency(['userid' => $u1->id, 'competencyid' => $c1->get_id(),
+            'status' => user_competency::STATUS_IDLE]);
+        $uc1b = $ccg->create_user_competency(['userid' => $u1->id, 'competencyid' => $c2->get_id(),
+            'status' => user_competency::STATUS_WAITING_FOR_REVIEW]);
+        $uc1c = $ccg->create_user_competency(['userid' => $u1->id, 'competencyid' => $c3->get_id(),
+            'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $reviewer->id]);
+        $uc2a = $ccg->create_user_competency(['userid' => $u2->id, 'competencyid' => $c1->get_id(),
+            'status' => user_competency::STATUS_WAITING_FOR_REVIEW]);
+        $uc2b = $ccg->create_user_competency(['userid' => $u2->id, 'competencyid' => $c2->get_id(),
+            'status' => user_competency::STATUS_IDLE]);
+        $uc2c = $ccg->create_user_competency(['userid' => $u2->id, 'competencyid' => $c3->get_id(),
+            'status' => user_competency::STATUS_IN_REVIEW]);
+
+        // The reviewer can review all plans waiting for review, or in review where they are the reviewer.
+        $this->setUser($reviewer);
+        $result = api::list_user_competencies_to_review();
+        $this->assertEquals(3, $result['count']);
+        $this->assertEquals($uc2a->get_id(), $result['competencies'][0]->usercompetency->get_id());
+        $this->assertEquals($uc1b->get_id(), $result['competencies'][1]->usercompetency->get_id());
+        $this->assertEquals($uc1c->get_id(), $result['competencies'][2]->usercompetency->get_id());
+
+        // The reviewer cannot view the plans when they do not have the permission in the user's context.
+        role_assign($roleprohibit, $reviewer->id, context_user::instance($u2->id)->id);
+        accesslib_clear_all_caches_for_unit_testing();
+        $result = api::list_user_competencies_to_review();
+        $this->assertEquals(2, $result['count']);
+        $this->assertEquals($uc1b->get_id(), $result['competencies'][0]->usercompetency->get_id());
+        $this->assertEquals($uc1c->get_id(), $result['competencies'][1]->usercompetency->get_id());
+    }
 }
index f280e5f..6be6bf0 100644 (file)
@@ -69,6 +69,18 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
     /** @var int User role id */
     protected $userrole = null;
 
+    /** @var stdClass $scale1 Scale */
+    protected $scale1 = null;
+
+    /** @var stdClass $scale2 Scale */
+    protected $scale2 = null;
+
+    /** @var stdClass $scale3 Scale */
+    protected $scale3 = null;
+
+    /** @var stdClass $scale4 Scale */
+    protected $scale4 = null;
+
     /** @var string scaleconfiguration */
     protected $scaleconfiguration1 = null;
 
@@ -153,31 +165,36 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->category = $category;
         $this->othercategory = $othercategory;
 
-        $this->getDataGenerator()->create_scale(array("id" => "1", "scale" => "value1, value2"));
-        $this->getDataGenerator()->create_scale(array("id" => "2", "scale" => "value3, value4"));
-        $this->getDataGenerator()->create_scale(array("id" => "3", "scale" => "value5, value6"));
-        $this->getDataGenerator()->create_scale(array("id" => "4", "scale" => "value7, value8"));
+        $this->scale1 = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2"));
+        $this->scale2 = $this->getDataGenerator()->create_scale(array("scale" => "value3, value4"));
+        $this->scale3 = $this->getDataGenerator()->create_scale(array("scale" => "value5, value6"));
+        $this->scale4 = $this->getDataGenerator()->create_scale(array("scale" => "value7, value8"));
 
-        $this->scaleconfiguration1 = '[{"scaleid":"1"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration1 = '[{"scaleid":"'.$this->scale1->id.'"},' .
+                '{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]';
-        $this->scaleconfiguration2 = '[{"scaleid":"2"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration2 = '[{"scaleid":"'.$this->scale2->id.'"},' .
+                '{"name":"value3","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]';
-        $this->scaleconfiguration3 = '[{"scaleid":"3"},{"name":"value5","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration3 = '[{"scaleid":"'.$this->scale3->id.'"},' .
+                '{"name":"value5","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value6","id":2,"scaledefault":0,"proficient":1}]';
-        $this->scaleconfiguration4 = '[{"scaleid":"4"},{"name":"value8","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration4 = '[{"scaleid":"'.$this->scale4->id.'"},'.
+                '{"name":"value8","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value8","id":2,"scaledefault":0,"proficient":1}]';
         accesslib_clear_all_caches_for_unit_testing();
     }
 
 
     protected function create_competency_framework($number = 1, $system = true) {
+        $scalename = 'scale' . $number;
         $scalepropname = 'scaleconfiguration' . $number;
         $framework = array(
             'shortname' => 'shortname' . $number,
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
             'descriptionformat' => FORMAT_HTML,
-            'scaleid' => $number,
+            'scaleid' => $this->$scalename->id,
             'scaleconfiguration' => $this->$scalepropname,
             'visible' => true,
             'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id
@@ -241,6 +258,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
     }
 
     protected function update_competency_framework($id, $number = 1, $system = true) {
+        $scalename = 'scale' . $number;
         $scalepropname = 'scaleconfiguration' . $number;
         $framework = array(
             'id' => $id,
@@ -248,7 +266,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
             'descriptionformat' => FORMAT_HTML,
-            'scaleid' => $number,
+            'scaleid' => $this->$scalename->id,
             'scaleconfiguration' => $this->$scalepropname,
             'visible' => true,
             'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id
@@ -316,7 +334,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -335,7 +353,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
 
@@ -358,7 +376,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'idnumber' => 'id;"number',
             'description' => 'de<>\\..scription',
             'descriptionformat' => FORMAT_HTML,
-            'scaleid' => 1,
+            'scaleid' => $this->scale1->id,
             'scaleconfiguration' => $this->scaleconfiguration1,
             'visible' => true,
             'contextid' => context_system::instance()->id
@@ -384,7 +402,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -410,7 +428,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber2', $result->idnumber);
         $this->assertEquals('description2', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(2, $result->scaleid);
+        $this->assertEquals($this->scale2->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
 
@@ -444,7 +462,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -470,7 +488,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber2', $result->idnumber);
         $this->assertEquals('description2', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(2, $result->scaleid);
+        $this->assertEquals($this->scale2->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
 
@@ -579,12 +597,12 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
 
         $s1 = $this->getDataGenerator()->create_scale();
 
-        $f1 = $lpg->create_framework(array('scaleid' => 1));
-        $f2 = $lpg->create_framework(array('scaleid' => 1));
+        $f1 = $lpg->create_framework(array('scaleid' => $s1->id));
+        $f2 = $lpg->create_framework(array('scaleid' => $s1->id));
         $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id()));
         $c2 = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id()));
 
-        $this->assertEquals(1, $f1->get_scaleid());
+        $this->assertEquals($s1->id, $f1->get_scaleid());
 
         // Make the scale of f2 being used.
         $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2->get_id()));
@@ -593,7 +611,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $result = $this->update_competency_framework($f1->get_id(), 3, true);
 
         $f1 = new \core_competency\competency_framework($f1->get_id());
-        $this->assertEquals(3, $f1->get_scaleid());
+        $this->assertEquals($this->scale3->id, $f1->get_scaleid());
 
         // Changing the framework where the scale is used.
         try {
@@ -645,7 +663,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -732,7 +750,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
index c37f06d..b9dc0fd 100644 (file)
@@ -392,6 +392,35 @@ class core_competency_persistent_testcase extends advanced_testcase {
         $this->assertFalse(core_competency_testable_persistent::record_exists($id));
     }
 
+    public function test_get_sql_fields() {
+        $expected = '' .
+            'c.id AS comp_id, ' .
+            'c.shortname AS comp_shortname, ' .
+            'c.idnumber AS comp_idnumber, ' .
+            'c.description AS comp_description, ' .
+            'c.descriptionformat AS comp_descriptionformat, ' .
+            'c.parentid AS comp_parentid, ' .
+            'c.path AS comp_path, ' .
+            'c.sortorder AS comp_sortorder, ' .
+            'c.competencyframeworkid AS comp_competencyframeworkid, ' .
+            'c.ruletype AS comp_ruletype, ' .
+            'c.ruleconfig AS comp_ruleconfig, ' .
+            'c.ruleoutcome AS comp_ruleoutcome, ' .
+            'c.scaleid AS comp_scaleid, ' .
+            'c.scaleconfiguration AS comp_scaleconfiguration, ' .
+            'c.timecreated AS comp_timecreated, ' .
+            'c.timemodified AS comp_timemodified, ' .
+            'c.usermodified AS comp_usermodified';
+        $this->assertEquals($expected, core_competency_testable_persistent::get_sql_fields('c', 'comp_'));
+    }
+
+    /**
+     * @expectedException               coding_exception
+     * @expectedExceptionMessageRegExp  /The alias .+ exceeds 30 characters/
+     */
+    public function test_get_sql_fields_too_long() {
+        core_competency_testable_persistent::get_sql_fields('c');
+    }
 }
 
 /**
diff --git a/course/classes/search/mycourse.php b/course/classes/search/mycourse.php
new file mode 100644 (file)
index 0000000..db136cd
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search area for Moodle courses I can access.
+ *
+ * @package    core_course
+ * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_course\search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for Moodle courses I can access.
+ *
+ * @package    core_course
+ * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mycourse extends \core_search\area\base {
+
+    /**
+     * The context levels the search implementation is working on.
+     *
+     * @var array
+     */
+    protected static $levels = [CONTEXT_COURSE];
+
+    /**
+     * Returns recordset containing required data for indexing courses.
+     *
+     * @param int $modifiedfrom timestamp
+     * @return \moodle_recordset
+     */
+    public function get_recordset_by_timestamp($modifiedfrom = 0) {
+        global $DB;
+        return $DB->get_recordset_select('course', 'timemodified >= ?', array($modifiedfrom));
+    }
+
+    /**
+     * Returns the document associated with this course.
+     *
+     * @param stdClass $record
+     * @param array    $options
+     * @return \core_search\document
+     */
+    public function get_document($record, $options = array()) {
+        try {
+            $context = \context_course::instance($record->id);
+        } catch (\moodle_exception $ex) {
+            // Notify it as we run here as admin, we should see everything.
+            debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
+                $ex->getMessage(), DEBUG_DEVELOPER);
+            return false;
+        }
+        // Prepare associative array with data from DB.
+        $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+        $doc->set('title', $record->fullname);
+        $doc->set('content', content_to_text($record->summary, $record->summaryformat));
+        $doc->set('contextid', $context->id);
+        $doc->set('courseid', $record->id);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+        $doc->set('modified', $record->timemodified);
+        $doc->set('description1', $record->shortname);
+
+        // Check if this document should be considered new.
+        if (isset($options['lastindexedtime']) && $options['lastindexedtime'] < $record->timecreated) {
+            // If the document was created after the last index time, it must be new.
+            $doc->set_is_new(true);
+        }
+
+        return $doc;
+    }
+
+    /**
+     * Whether the user can access the document or not.
+     *
+     * @param int $id The course instance id.
+     * @return int
+     */
+    public function check_access($id) {
+        global $DB;
+        $course = $DB->get_record('course', array('id' => $id));
+        if (!$course) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        if (can_access_course($course)) {
+            return \core_search\manager::ACCESS_GRANTED;
+        }
+        return \core_search\manager::ACCESS_DENIED;
+    }
+
+    /**
+     * Link to the course.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_doc_url(\core_search\document $doc) {
+        return $this->get_context_url($doc);
+    }
+
+    /**
+     * Link to the course.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_context_url(\core_search\document $doc) {
+        return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
+    }
+}
index 3b322b0..dddbf45 100644 (file)
@@ -1289,6 +1289,14 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
                 // otherwise append the link url to the module name.
                 $item->name = (count($items) == 1 &&
                     $item->link->out() === $defaultmodule->link->out()) ? $modname : $modname . ':' . $item->link;
+
+                // If the module provides the helptext property, append it to the help text to match the look and feel
+                // of the default course modules.
+                if (isset($item->help) && isset($item->helplink)) {
+                    $linktext = get_string('morehelp');
+                    $item->help .= html_writer::tag('div',
+                        $OUTPUT->doc_link($item->helplink, $linktext, true), array('class' => 'helpdoclink'));
+                }
                 $modlist[$course->id][$modname][$item->name] = $item;
             }
             $return += $modlist[$course->id][$modname];
index 26400e3..f0d4869 100644 (file)
@@ -32,13 +32,6 @@ require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php'
 
 class core_course_courselib_testcase extends advanced_testcase {
 
-    /**
-     * Tidy up open files that may be left open.
-     */
-    protected function tearDown() {
-        gc_collect_cycles();
-    }
-
     /**
      * Set forum specific test values for calling create_module().
      *
@@ -1917,7 +1910,6 @@ class core_course_courselib_testcase extends advanced_testcase {
         $filepath = $CFG->dataroot . '/temp/backup/test-restore-course-event';
         $file->extract_to_pathname($fp, $filepath);
         $bc->destroy();
-        unset($bc);
 
         // Now we want to catch the restore course event.
         $sink = $this->redirectEvents();
@@ -1955,7 +1947,6 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         // Destroy the resource controller since we are done using it.
         $rc->destroy();
-        unset($rc);
     }
 
     /**
index 9ca911b..9ad5d73 100644 (file)
@@ -47,13 +47,6 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         require_once($CFG->dirroot . '/course/externallib.php');
     }
 
-    /**
-     * Tidy up open files that may be left open.
-     */
-    protected function tearDown() {
-        gc_collect_cycles();
-    }
-
     /**
      * Test create_categories
      */
index f794e66..1b051ef 100644 (file)
@@ -35,7 +35,7 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  * @copyright  2016 Frédéric Massart - FMCorz.net
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_restore_backup_testcase extends advanced_testcase {
+class core_course_restore_testcase extends advanced_testcase {
 
     /**
      * Backup a course and return its backup ID.
diff --git a/course/tests/search_test.php b/course/tests/search_test.php
new file mode 100644 (file)
index 0000000..6750fbf
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course global search unit tests.
+ *
+ * @package     core
+ * @category    phpunit
+ * @copyright   2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
+
+/**
+ * Provides the unit tests for course global search.
+ *
+ * @package     core
+ * @category    phpunit
+ * @copyright   2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_search_testcase extends advanced_testcase {
+
+    /**
+     * @var string Area id
+     */
+    protected $mycoursesareaid = null;
+
+    public function setUp() {
+        $this->resetAfterTest(true);
+        set_config('enableglobalsearch', true);
+
+        $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
+
+        // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
+        $search = testable_core_search::instance();
+    }
+
+    /**
+     * Indexing my courses contents.
+     *
+     * @return void
+     */
+    public function test_mycourses_indexing() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
+
+        $record = new stdClass();
+        $record->course = $course1->id;
+
+        // All records.
+        $recordset = $searcharea->get_recordset_by_timestamp(0);
+        $this->assertTrue($recordset->valid());
+        $nrecords = 0;
+        foreach ($recordset as $record) {
+            $this->assertInstanceOf('stdClass', $record);
+            $doc = $searcharea->get_document($record);
+            $this->assertInstanceOf('\core_search\document', $doc);
+            $nrecords++;
+        }
+        // If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
+        $recordset->close();
+        $this->assertEquals(3, $nrecords);
+
+        // The +2 is to prevent race conditions.
+        $recordset = $searcharea->get_recordset_by_timestamp(time() + 2);
+
+        // No new records.
+        $this->assertFalse($recordset->valid());
+        $recordset->close();
+    }
+
+    /**
+     * Document contents.
+     *
+     * @return void
+     */
+    public function test_mycourses_document() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
+
+        $user = self::getDataGenerator()->create_user();
+        $course = self::getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'teacher');
+
+        $doc = $searcharea->get_document($course);
+        $this->assertInstanceOf('\core_search\document', $doc);
+        $this->assertEquals($course->id, $doc->get('itemid'));
+        $this->assertEquals($this->mycoursesareaid . '-' . $course->id, $doc->get('id'));
+        $this->assertEquals($course->id, $doc->get('courseid'));
+        $this->assertFalse($doc->is_set('userid'));
+        $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+        $this->assertEquals($course->fullname, $doc->get('title'));
+
+        // Not nice. Applying \core_search\document::set line breaks clean up.
+        $summary = preg_replace("/\s+/", ' ', trim(content_to_text($course->summary, $course->summaryformat), "\r\n"));
+        $this->assertEquals($summary, $doc->get('content'));
+        $this->assertEquals($course->shortname, $doc->get('description1'));
+    }
+
+    /**
+     * Document accesses.
+     *
+     * @return void
+     */
+    public function test_mycourses_access() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course(array('visible' => 0));
+        $course3 = self::getDataGenerator()->create_course();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'teacher');
+        $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
+
+        $this->setUser($user1);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course2->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123));
+
+        $this->setUser($user2);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
+    }
+}
index 8f8347b..8db19d9 100644 (file)
@@ -651,9 +651,7 @@ class core_enrol_external extends external_api {
         global $DB;
 
         $params = self::validate_parameters(self::get_course_enrolment_methods_parameters(), array('courseid' => $courseid));
-
-        // Note that we can't use validate_context because the user is not enrolled in the course.
-        require_login(null, false, null, false, true);
+        self::validate_context(context_system::instance());
 
         $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST);
         $context = context_course::instance($course->id);
index a441ef8..656c295 100644 (file)
@@ -71,7 +71,7 @@ class enrol_guest_external extends external_api {
             throw new moodle_exception('invaliddata', 'error');
         }
 
-        require_login(null, false, null, false, true);
+        self::validate_context(context_system::instance());
         $enrolinstance = $DB->get_record('enrol', array('id' => $params['instanceid']), '*', MUST_EXIST);
 
         $course = $DB->get_record('course', array('id' => $enrolinstance->courseid), '*', MUST_EXIST);
index efe55ff..8b2cd1e 100644 (file)
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.';
+$string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.';
 $string['enrolenddate'] = 'End date';
 $string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
 $string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
-$string['enrolisdisabled'] = 'The LTI enrolment plugin is disabled.';
+$string['enrolisdisabled'] = 'The \'Publish as LTI tool\' plugin is disabled.';
 $string['enrolperiod'] = 'Enrolment duration';
 $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.';
 $string['enrolmentfinished'] = 'Enrolment finished.';
 $string['enrolmentnotstarted'] = 'Enrolment has not started.';
 $string['enrolstartdate'] = 'Start date';
 $string['enrolstartdate_help'] = 'If enabled, users can access from this date onward only.';
-$string['globalsharedsecret'] = 'Global shared secret';
+$string['frameembeddingnotenabled'] = 'To access the tool, please follow the link below.';
 $string['gradesync'] = 'Grade synchronisation';
-$string['gradesync_help'] = 'This determines if we want grade synchronisation to occur.';
-$string['maxenrolled'] = 'Maximum enrolled';
-$string['maxenrolled_help'] = 'Specifies the maximum number of users that can access from the remote system. The value \'0\' means there is no limit.';
-$string['maxenrolledreached'] = 'Maximum number of users allowed to access was already reached.';
-$string['membersync'] = 'Member synchronisation';
-$string['membersync_help'] = 'This determines if we want member synchronisation to occur.';
-$string['membersyncmode'] = 'Members synchronisation mode';
-$string['membersyncmode_help'] = 'This setting determines what we should do when synchronising members.';
-$string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing members';
-$string['membersyncmodeenrolnew'] = 'Enrol new members';
-$string['membersyncmodeunenrolmissing'] = 'Unenrol missing members';
+$string['gradesync_help'] = 'Whether grades from the tool are sent to the remote system (LTI consumer).';
+$string['maxenrolled'] = 'Maximum enrolled users';
+$string['maxenrolled_help'] = 'The maximum number of remote users who can access the tool. If set to zero, the number of enrolled users is unlimited.';
+$string['maxenrolledreached'] = 'The maximum number of remote users allowed to access the tool has been reached.';
+$string['membersync'] = 'User synchronisation';
+$string['membersync_help'] = 'Whether an account is created for each remote user and the user is enrolled in this course.';
+$string['membersyncmode'] = 'User synchronisation mode';
+$string['membersyncmode_help'] = 'Whether remote users should be enrolled and/or unenrolled from this course.';
+$string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing users';
+$string['membersyncmodeenrolnew'] = 'Enrol new users';
+$string['membersyncmodeunenrolmissing'] = 'Unenrol missing users';
 $string['notoolsprovided'] = 'No tools provided';
-$string['lti:config'] = 'Configure LTI enrol instances';
+$string['lti:config'] = 'Configure \'Publish as LTI tool\' instances';
 $string['lti:unenrol'] = 'Unenrol users from the course';
-$string['pluginname'] = 'Shared external tool';
-$string['pluginname_desc'] = 'The shared external tool plugin allows externals users to access a course or an activity via a unique link - this requires the LTI authentication plugin to be enabled.';
+$string['opentool'] = 'Open tool';
+$string['pluginname'] = 'Publish as LTI tool';
+$string['pluginname_desc'] = 'The \'Publish as LTI tool\' plugin, together with the LTI authentication plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.';
 $string['remotesystem'] = 'Remote system';
-$string['requirecompletion'] = 'Require the course or activity to be completed before sending the grades';
-$string['roleinstructor'] = 'Role for instructor';
-$string['roleinstructor_help'] = 'This is the role that will be assigned at the context of the tool specificed to LTI consumer instructor.';
-$string['rolelearner'] = 'Role for learner';
-$string['rolelearner_help'] = 'This is the role that will be assigned at the context of the tool specificed to the LTI consumer student.';
+$string['requirecompletion'] = 'Require course or activity completion prior to grade synchronisation';
+$string['roleinstructor'] = 'Role for teacher';
+$string['roleinstructor_help'] = 'The role assigned in the tool to the remote teacher.';
+$string['rolelearner'] = 'Role for student';
+$string['rolelearner_help'] = 'The role assigned in the tool to the remote student.';
 $string['secret'] = 'Secret';
-$string['secret_help'] = 'This is the secret that is shared with the LTI consumer in order for them to access this tool';
-$string['sharedexternaltools'] = 'Shared external tools';
-$string['syncsettings'] = 'Synchronisation settings';
-$string['tooldoesnotexist'] = 'The requested tool does not exist.';
-$string['tasksyncgrades'] = 'Handles syncing grades with the consumer';
-$string['tasksyncmembers'] = 'handles syncing members with the consumer';
-$string['toolsprovided'] = 'Tools provided';
-$string['tooltobeprovided'] = 'Tool to be provided';
+$string['secret_help'] = 'A string of characters which is shared with the remote system (LTI consumer) to provide access to the tool.';
+$string['sharedexternaltools'] = 'Published as LTI tools';
+$string['tasksyncgrades'] = 'Publish as LTI tool grade sync';
+$string['tasksyncmembers'] = 'Publish as LTI tool users sync';
+$string['toolsprovided'] = 'Published tools';
+$string['tooltobeprovided'] = 'Tool to be published';
 $string['userdefaultvalues'] = 'User default values';
index 6c92669..b6d31a0 100644 (file)
@@ -26,6 +26,20 @@ defined('MOODLE_INTERNAL') || die;
 
 if ($ADMIN->fulltree) {
 
+    $settings->add(new admin_setting_heading('enrol_lti_settings', '', get_string('pluginname_desc', 'enrol_lti')));
+
+    if (!is_enabled_auth('lti')) {
+        $notify = new \core\output\notification(get_string('authltimustbeenabled', 'enrol_lti'),
+            \core\output\notification::NOTIFY_WARNING);
+        $settings->add(new admin_setting_heading('enrol_lti_enable_auth_lti', '', $OUTPUT->render($notify)));
+    }
+
+    if (empty($CFG->allowframembedding)) {
+        $notify = new \core\output\notification(get_string('allowframeembedding', 'enrol_lti'),
+            \core\output\notification::NOTIFY_WARNING);
+        $settings->add(new admin_setting_heading('enrol_lti_enable_embedding', '', $OUTPUT->render($notify)));
+    }
+
     $settings->add(new admin_setting_heading('enrol_lti_user_default_values',
         get_string('userdefaultvalues', 'enrol_lti'), ''));
 
index e4ca6d5..eeb24b1 100644 (file)
@@ -17,8 +17,8 @@ Feature: Check that settings are adhered to when creating an enrolment plugin
       | teacher1 | C1 | editingteacher |
     And I log in as "admin"
     And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments"
-    And I click on "Enable" "link" in the "Shared external tool" "table_row"
-    And I navigate to "Shared external tool" node in "Site administration > Plugins > Enrolments"
+    And I click on "Enable" "link" in the "Publish as LTI tool" "table_row"
+    And I navigate to "Publish as LTI tool" node in "Site administration > Plugins > Enrolments"
     And I set the following fields to these values:
       | Email display       | Allow everyone to see my email address |
       | City/town           | Perth                                  |
@@ -32,7 +32,7 @@ Feature: Check that settings are adhered to when creating an enrolment plugin
     Given I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I select "Shared external tool" from the "Add method" singleselect
+    And I select "Publish as LTI tool" from the "Add method" singleselect
     When I expand all fieldsets
     Then the field "Email display" matches value "Allow everyone to see my email address"
     And the field "City/town" matches value "Perth"
@@ -46,7 +46,7 @@ Feature: Check that settings are adhered to when creating an enrolment plugin
       | Timezone            | America/Vancouver                   |
       | Institution         | Moodle Pty Ltd - remote             |
     And I press "Add method"
-    And I click on "Edit" "link" in the "Shared external tool" "table_row"
+    And I click on "Edit" "link" in the "Publish as LTI tool" "table_row"
     And the field "Email display" matches value "Hide my email address from everyone"
     And the field "City/town" matches value "Whistler"
     And the field "Select a country" matches value "Canada"
index 71b28e3..46b553f 100644 (file)
@@ -16,7 +16,7 @@ Feature: Check that the page listing the shared external tools is functioning as
       | teacher1 | C1 | editingteacher |
     And I log in as "admin"
     And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments"
-    And I click on "Enable" "link" in the "Shared external tool" "table_row"
+    And I click on "Enable" "link" in the "Publish as LTI tool" "table_row"
     And I log out
 
   Scenario: I want to edit an external tool
@@ -27,12 +27,12 @@ Feature: Check that the page listing the shared external tools is functioning as
       | Assignment name | Test assignment name |
       | Description | Submit your online text |
     And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I select "Shared external tool" from the "Add method" singleselect
+    And I select "Publish as LTI tool" from the "Add method" singleselect
     And I set the following fields to these values:
       | Custom instance name | Assignment - LTI |
-      | Tool to be provided | Test assignment name |
+      | Tool to be published | Test assignment name |
     And I press "Add method"
-    And I navigate to "Shared external tools" node in "Course administration"
+    And I navigate to "Published as LTI tools" node in "Course administration"
     And I should see "Assignment - LTI" in the ".generaltable" "css_element"
     When I click on "Disable" "link" in the "Assignment - LTI" "table_row"
     Then ".dimmed_text" "css_element" should exist in the "Assignment - LTI" "table_row"
@@ -41,7 +41,7 @@ Feature: Check that the page listing the shared external tools is functioning as
     And I click on "Edit" "link" in the "Assignment - LTI" "table_row"
     And I set the following fields to these values:
       | Custom instance name | Course - LTI |
-      | Tool to be provided | Course |
+      | Tool to be published | Course |
     And I press "Save changes"
     And I should see "Course - LTI" in the ".generaltable" "css_element"
     And I click on "Delete" "link" in the "Course - LTI" "table_row"
index e267341..f7eab43 100644 (file)
@@ -204,8 +204,15 @@ if ($ltirequest->valid) {
     // Finalise the user log in.
     complete_user_login($user);
 
-    // All done, redirect the user to where they want to go.
-    redirect($urltogo);
+    if (empty($CFG->allowframembedding)) {
+        // Provide an alternative link.
+        $stropentool = get_string('opentool', 'enrol_lti');
+        echo html_writer::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti'));
+        echo html_writer::link($urltogo, $stropentool, array('target' => '_blank'));
+    } else {
+        // All done, redirect the user to where they want to go.
+        redirect($urltogo);
+    }
 } else {
     echo $ltirequest->message;
 }
index b72fb3b..9006e62 100644 (file)
@@ -67,8 +67,7 @@ class enrol_self_external extends external_api {
             throw new moodle_exception('invaliddata', 'error');
         }
 
-        // Note that we can't use validate_context because the user is not enrolled in the course.
-        require_login(null, false, null, false, true);
+        self::validate_context(context_system::instance());
 
         $enrolinstance = $DB->get_record('enrol', array('id' => $params['instanceid']), '*', MUST_EXIST);
         $course = $DB->get_record('course', array('id' => $enrolinstance->courseid), '*', MUST_EXIST);
@@ -146,8 +145,7 @@ class enrol_self_external extends external_api {
 
         $course = get_course($params['courseid']);
         $context = context_course::instance($course->id);
-        // Note that we can't use validate_context because the user is not enrolled in the course.
-        require_login(null, false, null, false, true);
+        self::validate_context(context_system::instance());
 
         if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
             throw new moodle_exception('coursehidden');
diff --git a/grade/grading/form/guide/db/upgrade.php b/grade/grading/form/guide/db/upgrade.php
new file mode 100644 (file)
index 0000000..eb50b3c
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file keeps track of upgrades to the marking guide grading method.
+ *
+ * @package   gradingform_guide
+ * @category  upgrade
+ * @copyright 2016 Jun Pataleta
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Marking guide grading method upgrade task.
+ *
+ * @param int $oldversion The version we are upgrading form.
+ * @return bool Returns true on success.
+ * @throws coding_exception
+ * @throws downgrade_exception
+ * @throws upgrade_exception
+ */
+function xmldb_gradingform_guide_upgrade($oldversion) {
+    global $DB;
+
+    if ($oldversion < 2016051100) {
+        // Clean up empty string or null marking guide comments.
+        $sql = $DB->sql_isempty('gradingform_guide_comments', 'description', true, true);
+        $sql .= " OR description IS NULL ";
+        $DB->delete_records_select('gradingform_guide_comments', $sql);
+        // Main savepoint reached.
+        upgrade_plugin_savepoint(true, 2016051100, 'gradingform', 'guide');
+    }
+
+    return true;
+}
index 25c797e..4a4e002 100644 (file)
@@ -213,6 +213,16 @@ class gradingform_guide_controller extends gradingform_controller {
                 $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE);
                 foreach ($commentfields as $key) {
                     if (array_key_exists($key, $comment)) {
+                        // Check if key is the comment's description.
+                        if ($key === 'description') {
+                            // Get a trimmed value for the comment description.
+                            $description = trim($comment[$key]);
+                            // Check if the comment description is empty.
+                            if (empty($description)) {
+                                // Continue to the next comment object if the description is empty.
+                                continue 2;
+                            }
+                        }
                         $data[$key] = $comment[$key];
                     }
                 }
index 928094e..53ce81e 100644 (file)
@@ -235,26 +235,30 @@ class gradingform_guide_renderer extends plugin_renderer_base {
             // Grading remark text area.
             $input = html_writer::tag('textarea', s($currentremark), $remarkparams);
 
-            // Frequently used comments chooser.
-            $chooserbuttonid = 'criteria-' . $criterion['id'] . '-commentchooser';
-            $commentchooserparams = array('id' => $chooserbuttonid, 'class' => 'commentchooser');
-            $commentchooser = html_writer::tag('button', get_string('insertcomment', 'gradingform_guide'), $commentchooserparams);
+            // Show the frequently-used comments chooser only if there are defined entries.
+            if (!empty($comments)) {
+                // Frequently used comments chooser.
+                $chooserbuttonid = 'criteria-' . $criterion['id'] . '-commentchooser';
+                $commentchooserparams = array('id' => $chooserbuttonid, 'class' => 'commentchooser');
+                $commentchooser = html_writer::tag('button', get_string('insertcomment', 'gradingform_guide'),
+                    $commentchooserparams);
+
+                // Option items for the frequently used comments chooser dialog.
+                $commentoptions = array();
+                foreach ($comments as $id => $comment) {
+                    $commentoption = new stdClass();
+                    $commentoption->id = $id;
+                    $commentoption->description = s($comment['description']);
+                    $commentoptions[] = $commentoption;
+                }
 
-            // Option items for the frequently used comments chooser dialog.
-            $commentoptions = array();
-            foreach ($comments as $id => $comment) {
-                $commentoption = new stdClass();
-                $commentoption->id = $id;
-                $commentoption->description = s($comment['description']);
-                $commentoptions[] = $commentoption;
+                // Include string for JS for the comment chooser title.
+                $PAGE->requires->string_for_js('insertcomment', 'gradingform_guide');
+                // Include comment_chooser module.
+                $PAGE->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
+                    array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
             }
 
-            // Include string for JS for the comment chooser title.
-            $PAGE->requires->string_for_js('insertcomment', 'gradingform_guide');
-            // Include comment_chooser module.
-            $PAGE->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
-                                         array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
-
             // Hidden marking guide remark label.
             $remarklabelparams = array(
                 'class' => 'hidden',
index 2724842..1608c38 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'gradingform_guide';
-$plugin->version    = 2015111600;
+$plugin->version    = 2016051100;
 $plugin->requires   = 2015111000;
 $plugin->maturity   = MATURITY_STABLE;
\ No newline at end of file
diff --git a/grade/tests/behat/grade_letter_boundary.feature b/grade/tests/behat/grade_letter_boundary.feature
new file mode 100644 (file)
index 0000000..210732a
--- /dev/null
@@ -0,0 +1,43 @@
+@core @core_grades
+Feature: We can customise the letter boundary of a course.
+  In order to change the letter boundary of a course
+  As a teacher
+  I need to add assessments to the gradebook.
+
+  @javascript
+  Scenario: I edit the letter boundaries of a course and grade a student.
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email | idnumber | alternatename |
+      | teacher1 | Teacher | 1 | teacher1@example.com | t1 | Terry         |
+      | student1 | Student | 1 | student1@example.com | s1 | Sally         |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | course | idnumber | name | intro | grade |
+      | assign | C1 | a1 | Test assignment one | Submit something! | 100 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I select "Course grade settings" from the "Grade report" singleselect
+    And I set the following fields to these values:
+      | Grade display type | Letter |
+    And I press "Save changes"
+    And I navigate to "Letters" node in "Grade administration"
+    And I follow "Edit grade letters"
+    And I set the following fields to these values:
+      | id_override | 1 |
+      | id_gradeboundary10 | 57 |
+    And I press "Save changes"
+    And I select "Grader report" from the "Grade report" singleselect
+    And I press "Turn editing on"
+    And I give the grade "57" to the user "Student 1" for the grade item "Test assignment one"
+    And I press "Save changes"
+    And I press "Turn editing off"
+    Then the following should exist in the "user-grades" table:
+      | -1-       | -4- | -5- |
+      | Student 1 | D   | D   |
\ No newline at end of file
diff --git a/grade/tests/behat/grade_letter_boundary_20160518.feature b/grade/tests/behat/grade_letter_boundary_20160518.feature
new file mode 100644 (file)
index 0000000..47fcb56
--- /dev/null
@@ -0,0 +1,44 @@
+@core @core_grades
+Feature: We can customise the letter boundary of a course.
+  In order to change the letter boundary of a course
+  As a teacher
+  I need to add assessments to the gradebook.
+
+  @javascript
+  Scenario: I edit the letter boundaries of a course and grade a student.
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And gradebook calculations for the course "C1" are frozen at version "20160518"
+    And the following "users" exist:
+      | username | firstname | lastname | email | idnumber | alternatename |
+      | teacher1 | Teacher | 1 | teacher1@example.com | t1 | Terry         |
+      | student1 | Student | 1 | student1@example.com | s1 | Sally         |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | course | idnumber | name | intro | grade |
+      | assign | C1 | a1 | Test assignment one | Submit something! | 100 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I select "Course grade settings" from the "Grade report" singleselect
+    And I set the following fields to these values:
+      | Grade display type | Letter |
+    And I press "Save changes"
+    And I navigate to "Letters" node in "Grade administration"
+    And I follow "Edit grade letters"
+    And I set the following fields to these values:
+      | id_override | 1 |
+      | id_gradeboundary10 | 57 |
+    And I press "Save changes"
+    And I select "Grader report" from the "Grade report" singleselect
+    And I press "Turn editing on"
+    And I give the grade "57" to the user "Student 1" for the grade item "Test assignment one"
+    And I press "Save changes"
+    And I press "Turn editing off"
+    Then the following should exist in the "user-grades" table:
+      | -1-       | -4- | -5- |
+      | Student 1 | F   | F   |
\ No newline at end of file
index a35d587..f823482 100644 (file)
@@ -34,7 +34,7 @@ $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 's';
 $string['cliincorrectvalueerror'] = 'Erro, valor incorreto "{$a->value}" para "{$a->option}"';
 $string['cliincorrectvalueretry'] = 'Valor incorreto, por favor tente novamente';
-$string['clitypevalue'] = 'Digite o valor';
+$string['clitypevalue'] = 'digite o valor';
 $string['clitypevaluedefault'] = 'digite o valor, pressione Enter para utilizar o valor padrão ({$a})';
 $string['cliunknowoption'] = 'Opções não reconhecidas:
   {$a}