Merge branch 'MDL-55340-39' of git://github.com/sarjona/moodle into MOODLE_39_STABLE
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Sep 2020 02:14:13 +0000 (10:14 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Sep 2020 02:14:13 +0000 (10:14 +0800)
50 files changed:
admin/tool/usertours/classes/manager.php
comment/classes/external.php
comment/comment_ajax.php
comment/lib.php
comment/tests/context_freeze_test.php [new file with mode: 0644]
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/ui/finalgrade.php
grade/report/singleview/tests/behat/bulk_insert_grades.feature
grade/report/singleview/tests/behat/singleview.feature
lang/en/error.php
lib/form/duration.php
lib/form/tests/duration_test.php
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js
lib/form/yui/src/dateselector/js/calendar.js
lib/grade/grade_grade.php
lib/grade/tests/grade_grade_test.php
mod/assign/feedback/file/importziplib.php
mod/assign/feedback/file/tests/importziplib_test.php [new file with mode: 0644]
mod/h5pactivity/mod_form.php
pix/i/completion-auto-enabled.png
pix/i/completion-auto-enabled.svg
pix/i/completion-auto-fail.png
pix/i/completion-auto-fail.svg
pix/i/completion-auto-n.png
pix/i/completion-auto-n.svg
pix/i/completion-auto-pass.png
pix/i/completion-auto-pass.svg
pix/i/completion-auto-y-override.png
pix/i/completion-auto-y-override.svg
pix/i/completion-auto-y.png
pix/i/completion-auto-y.svg
pix/i/completion-manual-enabled.png
pix/i/completion-manual-enabled.svg
pix/i/completion-manual-n.png
pix/i/completion-manual-n.svg
pix/i/completion-manual-y-override.png
pix/i/completion-manual-y-override.svg
pix/i/completion-manual-y.png
pix/i/completion-manual-y.svg
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/build/form-display-errors.min.js.map
theme/boost/amd/src/form-display-errors.js
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/course.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css

index 4a9f5cf..6741568 100644 (file)
@@ -138,13 +138,28 @@ class manager {
      */
     const CONFIG_SHIPPED_VERSION = 'shipped_version';
 
+    /**
+     * Helper method to initialize admin page, setting appropriate extra URL parameters
+     *
+     * @param string $action
+     */
+    protected function setup_admin_externalpage(string $action): void {
+        admin_externalpage_setup('tool_usertours/tours', '', array_filter([
+            'action' => $action,
+            'id' => optional_param('id', 0, PARAM_INT),
+            'tourid' => optional_param('tourid', 0, PARAM_INT),
+            'direction' => optional_param('direction', 0, PARAM_INT),
+        ]));
+    }
+
     /**
      * This is the entry point for this controller class.
      *
      * @param   string  $action     The action to perform.
      */
     public function execute($action) {
-        admin_externalpage_setup('tool_usertours/tours');
+        $this->setup_admin_externalpage($action);
+
         // Add the main content.
         switch($action) {
             case self::ACTION_NEWTOUR:
index 41a8b41..1eb3d8e 100644 (file)
@@ -337,7 +337,7 @@ class core_comment_external extends external_api {
             $args->area      = $commentrecord->commentarea;
             $manager = new comment($args);
 
-            if ($commentrecord->userid != $USER->id && !$manager->can_delete($commentrecord->id)) {
+            if (!$manager->can_delete($commentrecord)) {
                 throw new comment_exception('nopermissiontodelentry');
             }
 
index c1fe37c..50caa94 100644 (file)
@@ -91,8 +91,8 @@ switch ($action) {
         }
         break;
     case 'delete':
-        $comment_record = $DB->get_record('comments', array('id'=>$commentid));
-        if ($manager->can_delete($commentid) || $comment_record->userid == $USER->id) {
+        $comment = $DB->get_record('comments', ['id' => $commentid]);
+        if ($manager->can_delete($comment)) {
             if ($manager->delete($commentid)) {
                 $result = array(
                     'client_id' => $client_id,
index 58f8151..65d81e3 100644 (file)
@@ -589,8 +589,7 @@ class comment {
             $c->avatar = $OUTPUT->user_picture($u, array('size'=>18));
             $c->userid = $u->id;
 
-            $candelete = $this->can_delete($c->id);
-            if (($USER->id == $u->id) || !empty($candelete)) {
+            if ($this->can_delete($c)) {
                 $c->delete = true;
             }
             $comments[] = $c;
@@ -800,16 +799,22 @@ class comment {
     /**
      * Delete a comment
      *
-     * @param  int $commentid
+     * @param  int|stdClass $comment The id of a comment, or a comment record.
      * @return bool
      */
-    public function delete($commentid) {
-        global $DB, $USER;
-        $candelete = has_capability('moodle/comment:delete', $this->context);
-        if (!$comment = $DB->get_record('comments', array('id'=>$commentid))) {
+    public function delete($comment) {
+        global $DB;
+        if (is_object($comment)) {
+            $commentid = $comment->id;
+        } else {
+            $commentid = $comment;
+            $comment = $DB->get_record('comments', ['id' => $commentid]);
+        }
+
+        if (!$comment) {
             throw new comment_exception('dbupdatefailed');
         }
-        if (!($USER->id == $comment->userid || !empty($candelete))) {
+        if (!$this->can_delete($comment)) {
             throw new comment_exception('nopermissiontocomment');
         }
         $DB->delete_records('comments', array('id'=>$commentid));
@@ -976,13 +981,35 @@ class comment {
     }
 
     /**
-     * Returns true if the user can delete this comment
-     * @param int $commentid
+     * Returns true if the user can delete this comment.
+     *
+     * The user can delete comments if it is one they posted and they can still make posts,
+     * or they have the capability to delete comments.
+     *
+     * A database call is avoided if a comment record is passed.
+     *
+     * @param int|stdClass $comment The id of a comment, or a comment record.
      * @return bool
      */
-    public function can_delete($commentid) {
+    public function can_delete($comment) {
+        global $USER, $DB;
+        if (is_object($comment)) {
+            $commentid = $comment->id;
+        } else {
+            $commentid = $comment;
+        }
+
         $this->validate(array('commentid'=>$commentid));
-        return has_capability('moodle/comment:delete', $this->context);
+
+        if (!is_object($comment)) {
+            // Get the comment record from the database.
+            $comment = $DB->get_record('comments', array('id' => $commentid), 'id, userid', MUST_EXIST);
+        }
+
+        $hascapability = has_capability('moodle/comment:delete', $this->context);
+        $owncomment = $USER->id == $comment->userid;
+
+        return ($hascapability || ($owncomment && $this->can_post()));
     }
 
     /**
diff --git a/comment/tests/context_freeze_test.php b/comment/tests/context_freeze_test.php
new file mode 100644 (file)
index 0000000..36f1d60
--- /dev/null
@@ -0,0 +1,166 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for comments when the context is frozen.
+ *
+ * @package    core_comment
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for comments when the context is frozen.
+ *
+ * @package    core_comment
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class comment_context_freeze_testcase extends advanced_testcase {
+    /**
+     * Creates a comment by a student.
+     *
+     * Returns:
+     * - The comment object
+     * - The sudent that wrote the comment
+     * - The arguments used to create the comment
+     *
+     * @param stdClass $course Moodle course from the datagenerator
+     * @return array
+     */
+    protected function create_student_comment_and_freeze_course($course): array {
+        set_config('contextlocking', 1);
+
+        $context = context_course::instance($course->id);
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $args = new stdClass;
+        $args->context = $context;
+        $args->course = $course;
+        $args->area = 'page_comments';
+        $args->itemid = 0;
+        $args->component = 'block_comments';
+        $args->linktext = get_string('showcomments');
+        $args->notoggle = true;
+        $args->autostart = true;
+        $args->displaycancel = false;
+
+        // Create a comment by the student.
+        $this->setUser($student);
+        $comment = new comment($args);
+        $newcomment = $comment->add('New comment');
+
+        // Freeze the context.
+        $this->setAdminUser();
+        $context->set_locked(true);
+
+        return [$newcomment, $student, $args];
+    }
+
+    /**
+     * Test that a student cannot delete their own comments in frozen contexts via the external service.
+     */
+    public function test_delete_student_external() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that a student cannot delete their own comment.
+        $this->setUser($student);
+        $studentcomment = new comment($args);
+        $this->assertFalse($studentcomment->can_delete($newcomment->id));
+        $this->assertFalse($studentcomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        core_comment_external::delete_comments([$newcomment->id]);
+    }
+
+    /**
+     * Test that a student cannot delete their own comments in frozen contexts.
+     */
+    public function test_delete_student() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that a student cannot delete their own comment.
+        $this->setUser($student);
+        $studentcomment = new comment($args);
+        $this->assertFalse($studentcomment->can_delete($newcomment->id));
+        $this->assertFalse($studentcomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontocomment', 'error'));
+        $studentcomment->delete($newcomment->id);
+    }
+
+    /**
+     * Test that an admin cannot delete comments in frozen contexts via the external service.
+     */
+    public function test_delete_admin_external() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that the admin user cannot delete the comment.
+        $admincomment = new comment($args);
+        $this->assertFalse($admincomment->can_delete($newcomment->id));
+        $this->assertFalse($admincomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        core_comment_external::delete_comments([$newcomment->id]);
+    }
+
+    /**
+     * Test that an admin cannot delete comments in frozen contexts.
+     */
+    public function test_delete_admin() {
+        global $CFG;
+        require_once($CFG->dirroot . '/comment/lib.php');
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        list($newcomment, $student, $args) = $this->create_student_comment_and_freeze_course($course);
+
+        // Check that the admin user cannot delete the comment.
+        $admincomment = new comment($args);
+        $this->assertFalse($admincomment->can_delete($newcomment->id));
+        $this->assertFalse($admincomment->can_post());
+        $this->expectException(comment_exception::class);
+        $this->expectExceptionMessage(get_string('nopermissiontocomment', 'error'));
+        $admincomment->delete($newcomment->id);
+    }
+}
index f3b4ad7..999fc6f 100644 (file)
@@ -164,7 +164,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
     public function original_headers() {
         return array(
             '', // For filter icon.
-            get_string('firstname') . ' (' . get_string('alternatename') . ') ' . get_string('lastname'),
+            get_string('fullnameuser', 'core'),
             get_string('range', 'grades'),
             get_string('grade', 'grades'),
             get_string('feedback', 'grades'),
@@ -198,8 +198,8 @@ class grade extends tablelike implements selectable_items, filterable_items {
             $lockicon = $OUTPUT->pix_icon('t/locked', 'grade is locked') . ' ';
         }
 
-        if (!empty($item->alternatename)) {
-            $fullname = $lockicon . $item->alternatename . ' (' . $item->firstname . ') ' . $item->lastname;
+        if (has_capability('moodle/site:viewfullnames', \context_course::instance($this->courseid))) {
+            $fullname = $lockicon . fullname($item, true);
         } else {
             $fullname = $lockicon . fullname($item);
         }
index fd5e026..5921fc2 100644 (file)
@@ -161,12 +161,12 @@ class finalgrade extends grade_attribute_format implements unique_value, be_disa
         }
 
         if ($errorstr) {
-            $user = $DB->get_record('user', array('id' => $userid), 'id, firstname, alternatename, lastname');
+            $user = get_complete_user_data('id', $userid);
             $gradestr = new stdClass;
-            if (!empty($user->alternatename)) {
-                $gradestr->username = $user->alternatename . ' (' . $user->firstname . ') ' . $user->lastname;
+            if (has_capability('moodle/site:viewfullnames', \context_course::instance($gradeitem->courseid))) {
+                $gradestr->username = fullname($user, true);
             } else {
-                $gradestr->username = $user->firstname . ' ' . $user->lastname;
+                $gradestr->username = fullname($user);
             }
             $gradestr->itemname = $this->grade->grade_item->get_name();
             $errorstr = get_string($errorstr, 'grades', $gradestr);
index fd1153a..faa5ba2 100644 (file)
@@ -42,33 +42,33 @@ Feature: We can bulk insert grades for students in a course
     And I am on "Course 1" course homepage
     And I navigate to "View > Grader report" in the course gradebook
     And I follow "Single view for Test assignment one"
-    Then the field "Grade for james (Student) 1" matches value "50.00"
-    And the field "Override for james (Student) 1" matches value "0"
+    Then the field "Grade for Student 1" matches value "50.00"
+    And the field "Override for Student 1" matches value "0"
     And I set the field "Perform bulk insert" to "1"
     And I set the field "Insert value" to "1.0"
     And I press "Save"
     And I press "Continue"
-    And the field "Grade for james (Student) 1" matches value "50.00"
-    And the field "Override for james (Student) 1" matches value "0"
-    And the field "Grade for holly (Student) 2" matches value "1.00"
-    And the field "Override for holly (Student) 2" matches value "1"
-    And the field "Grade for anna (Student) 3" matches value "1.00"
-    And the field "Override for anna (Student) 3" matches value "1"
-    And the field "Grade for zac (Student) 4" matches value "1.00"
-    And the field "Override for zac (Student) 4" matches value "1"
+    And the field "Grade for Student 1" matches value "50.00"
+    And the field "Override for Student 1" matches value "0"
+    And the field "Grade for Student 2" matches value "1.00"
+    And the field "Override for Student 2" matches value "1"
+    And the field "Grade for Student 3" matches value "1.00"
+    And the field "Override for Student 3" matches value "1"
+    And the field "Grade for Student 4" matches value "1.00"
+    And the field "Override for Student 4" matches value "1"
     And I set the field "For" to "All grades"
     And I set the field "Perform bulk insert" to "1"
     And I set the field "Insert value" to "2.0"
     And I press "Save"
     And I press "Continue"
-    And the field "Grade for james (Student) 1" matches value "2.00"
-    And the field "Override for james (Student) 1" matches value "1"
-    And the field "Grade for holly (Student) 2" matches value "2.00"
-    And the field "Override for holly (Student) 2" matches value "1"
-    And the field "Grade for anna (Student) 3" matches value "2.00"
-    And the field "Override for anna (Student) 3" matches value "1"
-    And the field "Grade for zac (Student) 4" matches value "2.00"
-    And the field "Override for zac (Student) 4" matches value "1"
+    And the field "Grade for Student 1" matches value "2.00"
+    And the field "Override for Student 1" matches value "1"
+    And the field "Grade for Student 2" matches value "2.00"
+    And the field "Override for Student 2" matches value "1"
+    And the field "Grade for Student 3" matches value "2.00"
+    And the field "Override for Student 3" matches value "1"
+    And the field "Grade for Student 4" matches value "2.00"
+    And the field "Override for Student 4" matches value "1"
 
   Scenario: I can bulk insert grades and check their override flags for user view.
     Given I log in as "teacher1"
@@ -110,8 +110,8 @@ Feature: We can bulk insert grades for students in a course
     And I set the field "Perform bulk insert" to "1"
     When I set the field "Insert value" to "-1"
     And I press "Save"
-    Then I should see "The grade entered for Test assignment one for james (Student) 1 is less than the minimum allowed"
-    And I should see "The grade entered for Test assignment one for holly (Student) 2 is less than the minimum allowed"
-    And I should see "The grade entered for Test assignment one for anna (Student) 3 is less than the minimum allowed"
-    And I should see "The grade entered for Test assignment one for zac (Student) 4 is less than the minimum allowed"
+    Then I should see "The grade entered for Test assignment one for Student 1 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for Student 2 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for Student 3 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for Student 4 is less than the minimum allowed"
     And I should see "Grades were set for 0 items"
index e18eb5c..b04c8a7 100644 (file)
@@ -9,13 +9,13 @@ Feature: We can use Single view
       | 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 | fred |
-      | teacher2 | No edit | 1 | teacher2@example.com | t2 | nick |
-      | student1 | Student | 1 | student1@example.com | s1 | james |
-      | student2 | Student | 2 | student1@example.com | s2 | holly |
-      | student3 | Student | 3 | student1@example.com | s3 | anna |
-      | student4 | Student | 4 | student1@example.com | s4 | zac |
+      | username | firstname | lastname    | email                | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic |
+      | teacher1 | Teacher   | 1           | teacher1@example.com | t1       |            | fred          |                   |                  |
+      | teacher2 | No edit   | 1           | teacher2@example.com | t2       |            | nick          |                   |                  |
+      | student1 | Grainne   | Beauchamp   | student1@example.com | s1       | Ann        | Jill          | Gronya            | Beecham          |
+      | student2 | Niamh     | Cholmondely | student2@example.com | s2       | Jane       | Nina          | Nee               | Chumlee          |
+      | student3 | Siobhan   | Desforges   | student3@example.com | s3       | Sarah      | Sev           | Shevon            | De-forjay        |
+      | student4 | Student   | 4           | student4@example.com | s4       |            | zac           |                   |                  |
     And the following "scales" exist:
       | name | scale |
       | Test Scale | Disappointing, Good, Very good, Excellent |
@@ -50,6 +50,9 @@ Feature: We can use Single view
       | capability                  | permission | role     | contextlevel  | reference |
       | moodle/grade:edit           | Allow      | teacher  | Course        | C1        |
       | gradereport/singleview:view | Allow      | teacher  | Course        | C1        |
+    And the following config values are set as admin:
+      | fullnamedisplay | firstnamephonetic,lastnamephonetic |
+      | alternativefullnameformat | middlename, alternatename, firstname, lastname |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     Given I navigate to "View > Grader report" in the course gradebook
@@ -57,7 +60,7 @@ Feature: We can use Single view
   @javascript
   Scenario: I can update grades, add feedback and exclude grades.
     Given I navigate to "View > Single view" in the course gradebook
-    And I select "Student 4" from the "Select user..." singleselect
+    And I select "Student" from the "Select user..." singleselect
     And I set the field "Override for Test assignment one" to "1"
     When I set the following fields to these values:
         | Grade for Test assignment one | 10.00 |
@@ -76,29 +79,29 @@ Feature: We can use Single view
     And the field "Grade for Test grade item" matches value "45.00"
     And the field "Grade for Course total" matches value "55.00"
     And I click on "Show grades for Test assignment three" "link"
-    And I click on "Override for james (Student) 1" "checkbox"
+    And I click on "Override for Ann, Jill, Grainne, Beauchamp" "checkbox"
     And I set the following fields to these values:
-        | Grade for james (Student) 1 | 12.05 |
-        | Feedback for james (Student) 1 | test data2 |
-    And I set the field "Exclude for holly (Student) 2" to "1"
+        | Grade for Ann, Jill, Grainne, Beauchamp | 12.05 |
+        | Feedback for Ann, Jill, Grainne, Beauchamp | test data2 |
+    And I set the field "Exclude for Jane, Nina, Niamh, Cholmondely" to "1"
     And I press "Save"
     Then I should see "Grades were set for 2 items"
     And I press "Continue"
-    And the field "Grade for james (Student) 1" matches value "12.05"
-    And the field "Exclude for holly (Student) 2" matches value "1"
+    And the field "Grade for Ann, Jill, Grainne, Beauchamp" matches value "12.05"
+    And the field "Exclude for Jane, Nina, Niamh, Cholmondely" matches value "1"
     And I select "new grade item 1" from the "Select grade item..." singleselect
-    And I set the field "Grade for james (Student) 1" to "Very good"
+    And I set the field "Grade for Ann, Jill, Grainne, Beauchamp" to "Very good"
     And I press "Save"
     Then I should see "Grades were set for 1 items"
     And I press "Continue"
     And the following should exist in the "generaltable" table:
         | First name (Alternate name) Surname | Grade |
-        | james (Student) 1 | Very good |
+        | Ann, Jill, Grainne, Beauchamp | Very good |
     And I log out
     And I log in as "teacher2"
     And I am on "Course 1" course homepage
     Given I navigate to "View > Single view" in the course gradebook
-    And I select "Student 4" from the "Select user..." singleselect
+    And I select "Student" from the "Select user..." singleselect
     And the "Exclude for Test assignment one" "checkbox" should be disabled
     And the "Override for Test assignment one" "checkbox" should be enabled
 
@@ -106,12 +109,12 @@ Feature: We can use Single view
     Given I follow "Single view for Test assignment one"
     Then I should see "Test assignment one"
     Then I navigate to "View > Grader report" in the course gradebook
-    And I follow "Single view for Student 1"
-    Then I should see "Student 1"
+    And I follow "Single view for Ann, Jill, Grainne, Beauchamp"
+    Then I should see "Gronya,Beecham"
 
   Scenario: I can bulk update grades.
-    Given I follow "Single view for Student 1"
-    Then I should see "Student 1"
+    Given I follow "Single view for Ann, Jill, Grainne, Beauchamp"
+    Then I should see "Gronya,Beecham"
     When I set the field "For" to "All grades"
     And I set the field "Insert value" to "1.0"
     And I set the field "Perform bulk insert" to "1"
@@ -119,12 +122,12 @@ Feature: We can use Single view
     Then I should see "Grades were set for 6 items"
 
   Scenario: Navigation works in the Single view.
-    Given I follow "Single view for Student 1"
-    Then I should see "Student 1"
-    And I follow "Student 2"
-    Then I should see "Student 2"
-    And I follow "Student 1"
-    Then I should see "Student 1"
+    Given I follow "Single view for Ann, Jill, Grainne, Beauchamp"
+    Then I should see "Gronya,Beecham"
+    And I follow "Nee,Chumlee"
+    Then I should see "Nee,Chumlee"
+    And I follow "Gronya,Beecham"
+    Then I should see "Gronya,Beecham"
     And I click on "Show grades for Test assignment four" "link"
     Then I should see "Test assignment four"
     And I follow "Test assignment three"
@@ -134,7 +137,7 @@ Feature: We can use Single view
 
   Scenario: Activities are clickable only when
     it has a valid activity page.
-    Given I follow "Single view for Student 1"
+    Given I follow "Single view for Ann, Jill, Grainne, Beauchamp"
     And "new grade item 1" "link" should not exist in the "//tbody//tr[position()=1]//td[position()=2]" "xpath_element"
     Then "Category total" "link" should not exist in the "//tbody//tr[position()=2]//td[position()=2]" "xpath_element"
     And "Course total" "link" should not exist in the "//tbody//tr[position()=last()]//td[position()=2]" "xpath_element"
index 3530052..e41176c 100644 (file)
@@ -440,7 +440,7 @@ $string['noparticipants'] = 'No participants found for this course';
 $string['noparticipatorycms'] = 'Sorry, but you have no participatory course modules to report on';
 $string['nopermissions'] = 'Sorry, but you do not currently have permissions to do that ({$a}).';
 $string['nopermissiontocomment'] = 'You can\'t add comments';
-$string['nopermissiontodelentry'] = 'You can\'t delete other people\'s entries!';
+$string['nopermissiontodelentry'] = 'You can\'t delete this comment!';
 $string['nopermissiontoeditcomment'] = 'You can\'t edit other people\'s comments!';
 $string['nopermissiontohide'] = 'No permission to hide!';
 $string['nopermissiontoimportact'] = 'You do not have the required permissions to import activities to this course';
index 20c0d05..58e7c6c 100644 (file)
@@ -34,7 +34,7 @@ require_once($CFG->libdir . '/form/text.php');
  * Duration element
  *
  * HTML class for a length of time. For example, 30 minutes of 4 days. The
- * values returned to PHP is the duration in seconds.
+ * values returned to PHP is the duration in seconds (an int rounded to the nearest second).
  *
  * @package   core_form
  * @category  form
@@ -301,6 +301,7 @@ class MoodleQuickForm_duration extends MoodleQuickForm_group {
         if ($this->_options['optional'] && empty($valuearray['enabled'])) {
             return $this->_prepareValue(0, $assoc);
         }
-        return $this->_prepareValue($valuearray['number'] * $valuearray['timeunit'], $assoc);
+        return $this->_prepareValue(
+                (int) round($valuearray['number'] * $valuearray['timeunit']), $assoc);
     }
 }
index 4ce9280..629ac4d 100644 (file)
@@ -47,7 +47,7 @@ class core_form_duration_testcase extends basic_testcase {
      *
      * @return MoodleQuickForm
      */
-    protected function get_test_form() {
+    protected function get_test_form(): MoodleQuickForm {
         $form = new temp_form_duration();
         return $form->getform();
     }
@@ -57,27 +57,26 @@ class core_form_duration_testcase extends basic_testcase {
      *
      * @return array with two elements, a MoodleQuickForm and a MoodleQuickForm_duration.
      */
-    protected function get_test_form_and_element() {
+    protected function get_test_form_and_element(): array {
         $mform = $this->get_test_form();
         $element = $mform->addElement('duration', 'duration');
         return [$mform, $element];
     }
 
     /**
-     * Testcase for testing contructor.
-     *
-     * @expectedException coding_exception
+     * Test the constructor error handling.
      */
-    public function test_constructor() {
+    public function test_constructor_rejects_invalid_unit(): void {
         // Test trying to create with an invalid unit.
         $mform = $this->get_test_form();
+        $this->expectException('coding_exception');
         $mform->addElement('duration', 'testel', null, ['defaultunit' => 123, 'optional' => false]);
     }
 
     /**
-     * Test contructor only some units.
+     * Test constructor only some units.
      */
-    public function test_constructor_limited_units() {
+    public function test_constructor_limited_units(): void {
         $mform = $this->get_test_form();
         $mform->addElement('duration', 'testel', null, ['units' => [MINSECS, 1], 'optional' => false]);
         $html = $mform->toHtml();
@@ -90,7 +89,7 @@ class core_form_duration_testcase extends basic_testcase {
     /**
      * Testcase for testing units (seconds, minutes, hours and days)
      */
-    public function test_get_units() {
+    public function test_get_units(): void {
         [$mform, $element] = $this->get_test_form_and_element();
         $units = $element->get_units();
         $this->assertEquals($units, [1 => get_string('seconds'), 60 => get_string('minutes'),
@@ -98,66 +97,95 @@ class core_form_duration_testcase extends basic_testcase {
     }
 
     /**
-     * Testcase for testing conversion of seconds to the best possible unit
+     * Data provider for {@see test_seconds_to_unit()}.
+     *
+     * @return array test cases.
      */
-    public function test_seconds_to_unit() {
-        [$mform, $element] = $this->get_test_form_and_element();
-        $this->assertEquals([0, MINSECS], $element->seconds_to_unit(0)); // Zero minutes, for a nice default unit.
-        $this->assertEquals([1, 1], $element->seconds_to_unit(1));
-        $this->assertEquals([3601, 1], $element->seconds_to_unit(3601));
-        $this->assertEquals([1, MINSECS], $element->seconds_to_unit(60));
-        $this->assertEquals([3, MINSECS], $element->seconds_to_unit(180));
-        $this->assertEquals([1, HOURSECS], $element->seconds_to_unit(3600));
-        $this->assertEquals([2, HOURSECS], $element->seconds_to_unit(7200));
-        $this->assertEquals([1, DAYSECS], $element->seconds_to_unit(86400));
-        $this->assertEquals([25, HOURSECS], $element->seconds_to_unit(90000));
+    public function seconds_to_unit_cases(): array {
+        return [
+            [[0, MINSECS], 0], // Zero minutes, for a nice default unit.
+            [[1, 1], 1],
+            [[3601, 1], 3601],
+            [[1, MINSECS], 60],
+            [[3, MINSECS], 180],
+            [[1, HOURSECS], 3600],
+            [[2, HOURSECS], 7200],
+            [[1, DAYSECS], 86400],
+            [[25, HOURSECS], 90000],
+        ];
+    }
 
+    /**
+     * Testcase for testing conversion of seconds to the best possible unit.
+     *
+     * @dataProvider seconds_to_unit_cases
+     * @param array $expected expected return value from seconds_to_unit
+     * @param int $seconds value to pass to seconds_to_unit
+     */
+    public function test_seconds_to_unit(array $expected, int $seconds): void {
+        [, $element] = $this->get_test_form_and_element();
+        $this->assertEquals($expected, $element->seconds_to_unit($seconds));
+    }
+
+    /**
+     * Testcase for testing conversion of seconds to the best possible unit with a non-default default unit.
+     */
+    public function test_seconds_to_unit_different_default_unit() {
+        $mform = $this->get_test_form();
         $element = $mform->addElement('duration', 'testel', null,
                 ['defaultunit' => DAYSECS, 'optional' => false]);
-        $this->assertEquals([0, DAYSECS], $element->seconds_to_unit(0)); // Zero minutes, for a nice default unit.
+        $this->assertEquals([0, DAYSECS], $element->seconds_to_unit(0));
+    }
+
+    /**
+     * Data provider for {@see test_export_value()}.
+     *
+     * @return array test cases.
+     */
+    public function export_value_cases(): array {
+        return [
+            [10, '10', 1],
+            [9, '9.3', 1],
+            [10, '9.5', 1],
+            [180, '3', MINSECS],
+            [90, '1.5', MINSECS],
+            [7200, '2', HOURSECS],
+            [86400, '1', DAYSECS],
+            [0, '0', HOURSECS],
+            [0, '10', 1, 0, true],
+            [20, '20', 1, 1, true],
+            [0, '10', 1, 0, true, ''],
+            [20, '20', 1, 1, true, ''],
+        ];
     }
 
     /**
      * Testcase to check generated timestamp
+     *
+     * @dataProvider export_value_cases
+     * @param int $expected Expected value returned by the element.
+     * @param string $number Number entered into the element.
+     * @param int $unit Unit selected in the element.
+     * @param int $enabled Whether the enabled checkbox on the form was selected. (Only used if $optional is true.)
+     * @param bool $optional Whether the element has the optional option on.
+     * @param string|null $label The element's label.
      */
-    public function test_exportValue() {
+    public function test_export_value(int $expected, string $number, int $unit, int $enabled = 0,
+            bool $optional = false, ?string $label = null): void {
+
+        // Create the test element.
         $mform = $this->get_test_form();
-        $el = $mform->addElement('duration', 'testel');
-        $values = ['testel' => ['number' => 10, 'timeunit' => 1]];
-        $this->assertEquals(['testel' => 10], $el->exportValue($values, true));
-        $this->assertEquals(10, $el->exportValue($values));
-        $values = ['testel' => ['number' => 3, 'timeunit' => MINSECS]];
-        $this->assertEquals(['testel' => 180], $el->exportValue($values, true));
-        $this->assertEquals(180, $el->exportValue($values));
-        $values = ['testel' => ['number' => 1.5, 'timeunit' => MINSECS]];
-        $this->assertEquals(['testel' => 90], $el->exportValue($values, true));
-        $this->assertEquals(90, $el->exportValue($values));
-        $values = ['testel' => ['number' => 2, 'timeunit' => HOURSECS]];
-        $this->assertEquals(['testel' => 7200], $el->exportValue($values, true));
-        $this->assertEquals(7200, $el->exportValue($values));
-        $values = ['testel' => ['number' => 1, 'timeunit' => DAYSECS]];
-        $this->assertEquals(['testel' => 86400], $el->exportValue($values, true));
-        $this->assertEquals(86400, $el->exportValue($values));
-        $values = ['testel' => ['number' => 0, 'timeunit' => HOURSECS]];
-        $this->assertEquals(['testel' => 0], $el->exportValue($values, true));
-        $this->assertEquals(0, $el->exportValue($values));
-
-        $el = $mform->addElement('duration', 'testel', null, ['optional' => true]);
-        $values = ['testel' => ['number' => 10, 'timeunit' => 1]];
-        $this->assertEquals(['testel' => 0], $el->exportValue($values, true));
-        $this->assertEquals(0, $el->exportValue($values));
-        $values = ['testel' => ['number' => 20, 'timeunit' => 1, 'enabled' => 1]];
-        $this->assertEquals(['testel' => 20], $el->exportValue($values, true));
-        $this->assertEquals(20, $el->exportValue($values));
-
-        // Optional element.
-        $el2 = $mform->addElement('duration', 'testel', '', ['optional' => true]);
-        $values = ['testel' => ['number' => 10, 'timeunit' => 1, 'enabled' => 1]];
-        $this->assertEquals(['testel' => 10], $el2->exportValue($values, true));
-        $this->assertEquals(10, $el2->exportValue($values));
-        $values = ['testel' => ['number' => 10, 'timeunit' => 1, 'enabled' => 0]];
-        $this->assertEquals(['testel' => 0], $el2->exportValue($values, true));
-        $this->assertEquals(null, $el2->exportValue($values));
+        $el = $mform->addElement('duration', 'testel', $label, $optional ? ['optional' => true] : []);
+
+        // Prepare the submitted values.
+        $values = ['testel' => ['number' => $number, 'timeunit' => $unit]];
+        if ($optional) {
+            $values['testel']['enabled'] = $enabled;
+        }
+
+        // Test.
+        $this->assertEquals(['testel' => $expected], $el->exportValue($values, true));
+        $this->assertEquals($expected, $el->exportValue($values));
     }
 }
 
index ab07f4e..d4cb9ed 100644 (file)
Binary files a/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js and b/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js differ
index 8835642..d7fc49d 100644 (file)
Binary files a/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js and b/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js differ
index ab07f4e..d4cb9ed 100644 (file)
Binary files a/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js and b/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js differ
index b3ba4b1..42b3967 100644 (file)
@@ -86,8 +86,8 @@ CALENDAR.prototype = {
             this.set_date_from_selects();
         }
         M.form.dateselector.currentowner = this;
-        M.form.dateselector.calendar.set('mindate', new Date(this.yearselect.firstOptionValue(), 0, 1));
-        M.form.dateselector.calendar.set('maxdate', new Date(this.yearselect.lastOptionValue(), 11, 31));
+        M.form.dateselector.calendar.set('minimumDate', new Date(this.yearselect.firstOptionValue(), 0, 1));
+        M.form.dateselector.calendar.set('maximumDate', new Date(this.yearselect.lastOptionValue(), 11, 31));
         M.form.dateselector.panel.show();
         M.form.dateselector.calendar.show();
         M.form.dateselector.fix_position();
index 832546a..13d2ad5 100644 (file)
@@ -252,6 +252,10 @@ class grade_grade extends grade_object {
             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
         }
 
+        if (empty($this->grade_item)) {
+            debugging("Missing grade item id $this->itemid", DEBUG_DEVELOPER);
+        }
+
         return $this->grade_item;
     }
 
@@ -1117,8 +1121,7 @@ class grade_grade extends grade_object {
         $success = parent::delete($source);
 
         // If the grade was deleted successfully trigger a grade_deleted event.
-        if ($success) {
-            $this->load_grade_item();
+        if ($success && !empty($this->grade_item)) {
             \core\event\grade_deleted::create_from_grade($this)->trigger();
         }
 
@@ -1157,8 +1160,10 @@ class grade_grade extends grade_object {
             return;
         }
 
-        // Load information about grade item
-        $this->load_grade_item();
+        // Load information about grade item, exit if the grade item is missing.
+        if (!$this->load_grade_item()) {
+            return;
+        }
 
         // Only course-modules have completion data
         if ($this->grade_item->itemtype!='mod') {
index d398033..5a003f5 100644 (file)
@@ -40,6 +40,7 @@ class core_grade_grade_testcase extends grade_base_testcase {
         $this->sub_test_grade_grade_set_hidden();
         $this->sub_test_grade_grade_is_hidden();
         $this->sub_test_grade_grade_deleted();
+        $this->sub_test_grade_grade_deleted_event();
     }
 
     protected function sub_test_grade_grade_construct() {
@@ -506,4 +507,67 @@ class core_grade_grade_testcase extends grade_base_testcase {
         $fs = get_file_storage();
         $fs->create_file_from_string($dummy, '');
     }
+
+    /**
+     * Tests grade_deleted event.
+     */
+    public function sub_test_grade_grade_deleted_event() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+
+        // Create the data we need for the tests.
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $c1 = $dg->create_course();
+        $a1 = $dg->create_module('assign', ['course' => $c1->id]);
+
+        $gi = new grade_item($dg->create_grade_item(
+            [
+                'courseid' => $c1->id,
+                'itemtype' => 'mod',
+                'itemmodule' => 'assign',
+                'iteminstance' => $a1->id
+            ]
+        ), false);
+
+        grade_update('mod/assign', $gi->courseid, $gi->itemtype, $gi->itemmodule, $gi->iteminstance,
+            $gi->itemnumber, ['userid' => $u1->id]);
+        grade_update('mod/assign', $gi->courseid, $gi->itemtype, $gi->itemmodule, $gi->iteminstance,
+            $gi->itemnumber, ['userid' => $u2->id]);
+
+        $gg = grade_grade::fetch(array('userid' => $u1->id, 'itemid' => $gi->id));
+        $this->assertEquals($u1->id, $gg->userid);
+        $gg->load_grade_item();
+        $this->assertEquals($gi->id, $gg->grade_item->id);
+
+        // Delete user with valid grade item.
+        $sink = $this->redirectEvents();
+        grade_user_delete($u1->id);
+        $events = $sink->get_events();
+        $event = reset($events);
+        $sink->close();
+        $this->assertInstanceOf('core\event\grade_deleted', $event);
+
+        $gg = grade_grade::fetch(array('userid' => $u2->id, 'itemid' => $gi->id));
+        $this->assertEquals($u2->id, $gg->userid);
+        $gg->load_grade_item();
+        $this->assertEquals($gi->id, $gg->grade_item->id);
+
+        // Delete grade item, mock up orphaned grade_grades.
+        $DB->delete_records('grade_items', ['id' => $gi->id]);
+        $gg = grade_grade::fetch(array('userid' => $u2->id, 'itemid' => $gi->id));
+        $this->assertEquals($u2->id, $gg->userid);
+
+        // No event is triggered and there is a debugging message.
+        $sink = $this->redirectEvents();
+        grade_user_delete($u2->id);
+        $this->assertDebuggingCalled("Missing grade item id $gi->id");
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertEmpty($events);
+
+        // The grade should be deleted.
+        $gg = grade_grade::fetch(array('userid' => $u2->id, 'itemid' => $gi->id));
+        $this->assertEmpty($gg);
+    }
 }
index 01d9a33..3cbe9f7 100644 (file)
@@ -59,33 +59,53 @@ class assignfeedback_file_zip_importer {
             return false;
         }
 
-        $info = explode('_', $fileinfo->get_filepath() . $fileinfo->get_filename(), 5);
+        // Break the full path-name into path parts.
+        $pathparts = explode('/', $fileinfo->get_filepath() . $fileinfo->get_filename());
 
-        if (count($info) < 5) {
-            return false;
-        }
+        while (!empty($pathparts)) {
+            // Get the next path part and break it up by underscores.
+            $pathpart = array_shift($pathparts);
+            $info = explode('_', $pathpart, 5);
 
-        $participantid = $info[1];
-        $filename = $info[4];
-        $plugin = $assignment->get_plugin_by_type($info[2], $info[3]);
+            if (count($info) < 5) {
+                continue;
+            }
 
-        if (!is_numeric($participantid)) {
-            return false;
-        }
+            // Check the participant id.
+            $participantid = $info[1];
 
-        if (!$plugin) {
-            return false;
-        }
+            if (!is_numeric($participantid)) {
+                continue;
+            }
 
-        // Convert to int.
-        $participantid += 0;
+            // Convert to int.
+            $participantid += 0;
 
-        if (empty($participants[$participantid])) {
-            return false;
+            if (empty($participants[$participantid])) {
+                continue;
+            }
+
+            // Set user, which is by reference, so is used by the calling script.
+            $user = $participants[$participantid];
+
+            // Set the plugin. This by reference, and is used by the calling script.
+            $plugin = $assignment->get_plugin_by_type($info[2], $info[3]);
+
+            if (!$plugin) {
+                continue;
+            }
+
+            // Take any remaining text in this part and put it back in the path parts array.
+            array_unshift($pathparts, $info[4]);
+
+            // Combine the remaining parts and set it as the filename.
+            // Note that filename is a 'by reference' variable, so we need to set it before returning.
+            $filename = implode('/', $pathparts);
+
+            return true;
         }
 
-        $user = $participants[$participantid];
-        return true;
+        return false;
     }
 
     /**
diff --git a/mod/assign/feedback/file/tests/importziplib_test.php b/mod/assign/feedback/file/tests/importziplib_test.php
new file mode 100644 (file)
index 0000000..009579b
--- /dev/null
@@ -0,0 +1,148 @@
+<?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/>.
+
+/**
+ * Unit tests for importziplib.
+ *
+ * @package    assignfeedback_file
+ * @copyright  2020 Eric Merrill <merrill@oakland.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/tests/generator.php');
+require_once($CFG->dirroot . '/mod/assign/feedback/file/importziplib.php');
+
+/**
+ * Unit tests for importziplib.
+ *
+ * @copyright  2020 Eric Merrill <merrill@oakland.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_importziplib_testcase extends advanced_testcase {
+
+    // Use the generator helper.
+    use mod_assign_test_generator;
+
+    /**
+     * Test the assignfeedback_file_zip_importer->is_valid_filename_for_import() method.
+     */
+    public function test_is_valid_filename_for_import() {
+        // Do the initial assign setup.
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $assign = $this->create_instance($course, [
+                'assignsubmission_onlinetext_enabled' => 1,
+                'assignfeedback_file_enabled' => 1,
+            ]);
+
+        // Create an online text submission.
+        $this->add_submission($student, $assign);
+
+        // Now onto the file work.
+        $fs = get_file_storage();
+
+        // Setup a basic file we will work with. We will keep renaming and repathing it.
+        $record = new stdClass;
+        $record->contextid = $assign->get_context()->id;
+        $record->component = 'assignfeedback_file';
+        $record->filearea  = ASSIGNFEEDBACK_FILE_FILEAREA;
+        $record->itemid    = $assign->get_user_grade($student->id, true)->id;
+        $record->filepath  = '/';
+        $record->filename  = '1.txt';
+        $record->source    = 'test';
+        $file = $fs->create_file_from_string($record, 'file content');
+
+        // The importer we will use.
+        $importer = new assignfeedback_file_zip_importer();
+
+        // Setup some variable we use.
+        $user = null;
+        $plugin = null;
+        $filename = '';
+
+        $allusers = $assign->list_participants(0, false);
+        $participants = array();
+        foreach ($allusers as $user) {
+            $participants[$assign->get_uniqueid_for_user($user->id)] = $user;
+        }
+
+        $file->rename('/import/', '.hiddenfile');
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        $file->rename('/import/', '~hiddenfile');
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        $file->rename('/import/some_path_here/', 'RandomFile.txt');
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        $file->rename('/import/', '~hiddenfile');
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        // Get the students assign id.
+        $studentid = $assign->get_uniqueid_for_user($student->id);
+
+        // Submissions are identified with the format:
+        // StudentName_StudentID_PluginType_Plugin_FilePathAndName.
+
+        // Test a string student id.
+        $badname = 'Student Name_StringID_assignsubmission_file_My_cool_filename.txt';
+        $file->rename('/import/', $badname);
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        // Test an invalid student id.
+        $badname = 'Student Name_' . ($studentid + 100) . '_assignsubmission_file_My_cool_filename.txt';
+        $file->rename('/import/', $badname);
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        // Test an invalid submission plugin.
+        $badname = 'Student Name_' . $studentid . '_assignsubmission_noplugin_My_cool_filename.txt';
+        $file->rename('/import/', $badname);
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertFalse($result);
+
+        // Test a basic, good file.
+        $goodbase = 'Student Name_' . $studentid . '_assignsubmission_file_';
+        $file->rename('/import/', $goodbase . "My_cool_filename.txt");
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertTrue($result);
+        $this->assertEquals($participants[$studentid], $user);
+        $this->assertEquals('My_cool_filename.txt', $filename);
+        $this->assertInstanceOf(assign_submission_file::class, $plugin);
+
+        // Test another good file, with some additional path and underscores.
+        $user = null;
+        $plugin = null;
+        $filename = '';
+        $file->rename('/import/some_path_here/' . $goodbase . '/some_path/', 'My File.txt');
+        $result = $importer->is_valid_filename_for_import($assign, $file, $participants, $user, $plugin, $filename);
+        $this->assertTrue($result);
+        $this->assertEquals($participants[$studentid], $user);
+        $this->assertEquals('/some_path/My File.txt', $filename);
+        $this->assertInstanceOf(assign_submission_file::class, $plugin);
+    }
+}
index 40e17dd..2de9409 100644 (file)
@@ -72,6 +72,7 @@ class mod_h5pactivity_mod_form extends moodleform_mod {
 
         $mform->addElement('filemanager', 'packagefile', get_string('package', 'mod_h5pactivity'), null, $options);
         $mform->addHelpButton('packagefile', 'package', 'mod_h5pactivity');
+        $mform->addRule('packagefile', null, 'required');
 
         // Add a link to the Content Bank if the user can access.
         $course = $this->get_course();
index c030e3d..68689f0 100644 (file)
Binary files a/pix/i/completion-auto-enabled.png and b/pix/i/completion-auto-enabled.png differ
index 43f2df4..0ee649a 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2zm13.7-1.1l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#999"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10,0V2H6V0Zm1,2h1a2,2,0,0,1,1.1.3,1.75,1.75,0,0,1,1.2-.4,2,2,0,0,1,1.4.6l.3.3V2a2,2,0,0,0-2-2H11Zm3,8h2V6.4l-2,2Zm0,1v1a2,2,0,0,1-2,2H11v2h3a2,2,0,0,0,2-2V11Zm-4,5V14H6v2ZM5,14H4a2,2,0,0,1-2-2V11H0v3a2,2,0,0,0,2,2H5ZM0,10H2V6H0ZM2,5V4A2,2,0,0,1,4,2H5V0H2A2,2,0,0,0,0,2V5ZM15.7,3.9,15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#949494"/></svg>
\ No newline at end of file
index c2766ac..badd1f0 100644 (file)
Binary files a/pix/i/completion-auto-fail.png and b/pix/i/completion-auto-fail.png differ
index f437d97..1929dfc 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c1.1 0 2 .9 2 2v1h2V2c0-1.1-.9-2-2-2h-3v2zm5 4h-2v4h2V6zm-2 5v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#999"/><path d="M10.2 8l2.6-2.6c.4-.4.4-1 0-1.4l-.8-.7c-.4-.4-1-.4-1.4 0L8 5.9 5.4 3.3c-.4-.4-1-.4-1.4 0l-.7.7c-.4.4-.4 1 0 1.4L5.9 8l-2.6 2.6c-.3.4-.3 1 0 1.4l.7.7c.4.4 1 .4 1.4 0L8 10.2l2.5 2.5c.4.4 1 .4 1.4 0l.7-.7c.4-.4.4-1 0-1.4L10.2 8z" fill="#FF403C"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10,0V2H6V0Zm1,2h1a2,2,0,0,1,2,2V5h2V2a2,2,0,0,0-2-2H11Zm5,4H14v4h2Zm-2,5v1a2,2,0,0,1-2,2H11v2h3a2,2,0,0,0,2-2V11Zm-4,5V14H6v2ZM5,14H4a2,2,0,0,1-2-2V11H0v3a2,2,0,0,0,2,2H5ZM0,10H2V6H0ZM2,5V4A2,2,0,0,1,4,2H5V0H2A2,2,0,0,0,0,2V5Z" fill="#949494"/><path d="M10.2,8l2.6-2.6a1,1,0,0,0,0-1.4L12,3.3a1,1,0,0,0-1.4,0L8,5.9,5.4,3.3A1,1,0,0,0,4,3.3L3.3,4a1,1,0,0,0,0,1.4L5.9,8,3.3,10.6a1.2,1.2,0,0,0,0,1.4l.7.7a1,1,0,0,0,1.4,0L8,10.2l2.5,2.5a1,1,0,0,0,1.4,0l.7-.7a1,1,0,0,0,0-1.4Z" fill="#ff403c"/></svg>
\ No newline at end of file
index c93c86b..01419b9 100644 (file)
Binary files a/pix/i/completion-auto-n.png and b/pix/i/completion-auto-n.png differ
index 56f0b03..e5d4138 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c1.1 0 2 .9 2 2v1h2V2c0-1.1-.9-2-2-2h-3v2zm5 4h-2v4h2V6zm-2 5v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#999"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10,0V2H6V0Zm1,2h1a2,2,0,0,1,2,2V5h2V2a2,2,0,0,0-2-2H11Zm5,4H14v4h2Zm-2,5v1a2,2,0,0,1-2,2H11v2h3a2,2,0,0,0,2-2V11Zm-4,5V14H6v2ZM5,14H4a2,2,0,0,1-2-2V11H0v3a2,2,0,0,0,2,2H5ZM0,10H2V6H0ZM2,5V4A2,2,0,0,1,4,2H5V0H2A2,2,0,0,0,0,2V5Z" fill="#949494"/></svg>
\ No newline at end of file
index f7384ad..0e88881 100644 (file)
Binary files a/pix/i/completion-auto-pass.png and b/pix/i/completion-auto-pass.png differ
index f644de7..9b91a5f 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#999"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#9C3"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10,0V2H6V0Zm1,2h1a2,2,0,0,1,1.1.3,1.75,1.75,0,0,1,1.2-.4,2,2,0,0,1,1.4.6l.3.3V2a2,2,0,0,0-2-2H11Zm3,8h2V6.4l-2,2Zm0,1v1a2,2,0,0,1-2,2H11v2h3a2,2,0,0,0,2-2V11Zm-4,5V14H6v2ZM5,14H4a2,2,0,0,1-2-2V11H0v3a2,2,0,0,0,2,2H5ZM0,10H2V6H0ZM2,5V4A2,2,0,0,1,4,2H5V0H2A2,2,0,0,0,0,2V5Z" fill="#949494"/><path d="M15.7,3.9,15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#79a128"/></svg>
\ No newline at end of file
index aeca4ed..7022a81 100644 (file)
Binary files a/pix/i/completion-auto-y-override.png and b/pix/i/completion-auto-y-override.png differ
index 13cf5d7..c849f00 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10,0V2H6V0Zm1,2h1a2,2,0,0,1,1.1.3,1.75,1.75,0,0,1,1.2-.4,2,2,0,0,1,1.4.6l.3.3V2a2,2,0,0,0-2-2H11Zm3,8h2V6.4l-2,2Zm0,1v1a2,2,0,0,1-2,2H11v2h3a2,2,0,0,0,2-2V11Zm-4,5V14H6v2ZM5,14H4a2,2,0,0,1-2-2V11H0v3a2,2,0,0,0,2,2H5ZM0,10H2V6H0ZM2,5V4A2,2,0,0,1,4,2H5V0H2A2,2,0,0,0,0,2V5Z" fill="#ff2727"/><path d="M15.7,3.9,15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#6393ee"/></svg>
\ No newline at end of file
index a5440d5..349f8bf 100644 (file)
Binary files a/pix/i/completion-auto-y.png and b/pix/i/completion-auto-y.png differ
index c734b49..9f2809f 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#999"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10,0V2H6V0Zm1,2h1a2,2,0,0,1,1.1.3,1.75,1.75,0,0,1,1.2-.4,2,2,0,0,1,1.4.6l.3.3V2a2,2,0,0,0-2-2H11Zm3,8h2V6.4l-2,2Zm0,1v1a2,2,0,0,1-2,2H11v2h3a2,2,0,0,0,2-2V11Zm-4,5V14H6v2ZM5,14H4a2,2,0,0,1-2-2V11H0v3a2,2,0,0,0,2,2H5ZM0,10H2V6H0ZM2,5V4A2,2,0,0,1,4,2H5V0H2A2,2,0,0,0,0,2V5Z" fill="#949494"/><path d="M15.7,3.9,15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#6393ee"/></svg>
\ No newline at end of file
index 1850d22..2b8389f 100644 (file)
Binary files a/pix/i/completion-manual-enabled.png and b/pix/i/completion-manual-enabled.png differ
index c415b90..dd45c3f 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M16 6.4V14c0 1.1-.9 2-2 2H2c-1.1 0-2-.9-2-2V2C0 .9.9 0 2 0h12c1.1 0 2 .9 2 2v.8l-.3-.3c-.4-.4-.9-.6-1.4-.6-.4 0-.9.1-1.2.4-.3-.2-.7-.3-1.1-.3H4c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V8.4l2-2zm-.3-2.5l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#999"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M16,6.4V14a2,2,0,0,1-2,2H2a2,2,0,0,1-2-2V2A2,2,0,0,1,2,0H14a2,2,0,0,1,2,2v.8l-.3-.3a2,2,0,0,0-1.4-.6,1.75,1.75,0,0,0-1.2.4A2,2,0,0,0,12,2H4A2,2,0,0,0,2,4v8a2,2,0,0,0,2,2h8a2,2,0,0,0,2-2V8.4Zm-.3-2.5L15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#949494"/></svg>
\ No newline at end of file
index 79e5c71..f783abf 100644 (file)
Binary files a/pix/i/completion-manual-n.png and b/pix/i/completion-manual-n.png differ
index 19785de..5c8d283 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 0H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V2c0-1.1-.9-2-2-2zm0 12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2v8z" fill="#999"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M14,0H2A2,2,0,0,0,0,2V14a2,2,0,0,0,2,2H14a2,2,0,0,0,2-2V2A2,2,0,0,0,14,0Zm0,12a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V4A2,2,0,0,1,4,2h8a2,2,0,0,1,2,2Z" fill="#949494"/></svg>
\ No newline at end of file
index bdbc46b..78d5b86 100644 (file)
Binary files a/pix/i/completion-manual-y-override.png and b/pix/i/completion-manual-y-override.png differ
index 69270ba..6ff1757 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 8.4V12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6.4l-2 2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M14,8.4V12a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V4A2,2,0,0,1,4,2h8a2,2,0,0,1,1.1.3,1.75,1.75,0,0,1,1.2-.4,2,2,0,0,1,1.4.6l.3.3V2a2,2,0,0,0-2-2H2A2,2,0,0,0,0,2V14a2,2,0,0,0,2,2H14a2,2,0,0,0,2-2V6.4Z" fill="#ff2727"/><path d="M15.7,3.9,15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#6393ee"/></svg>
\ No newline at end of file
index d1c85ee..7c79094 100644 (file)
Binary files a/pix/i/completion-manual-y.png and b/pix/i/completion-manual-y.png differ
index 8125e80..b2a39cd 100644 (file)
@@ -1,3 +1 @@
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
-       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
-]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 8.4V12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6.4l-2 2z" fill="#999"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M14,8.4V12a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V4A2,2,0,0,1,4,2h8a2,2,0,0,1,1.1.3,1.75,1.75,0,0,1,1.2-.4,2,2,0,0,1,1.4.6l.3.3V2a2,2,0,0,0-2-2H2A2,2,0,0,0,0,2V14a2,2,0,0,0,2,2H14a2,2,0,0,0,2-2V6.4Z" fill="#949494"/><path d="M15.7,3.9,15,3.2a1,1,0,0,0-1.4,0l-6,6L5.4,7A1.2,1.2,0,0,0,4,7l-.7.7a1,1,0,0,0,0,1.4l3.6,3.6a1,1,0,0,0,1.4,0l7.4-7.4A1,1,0,0,0,15.7,3.9Z" fill="#6293ee"/></svg>
\ No newline at end of file
index cd80f47..e169aac 100644 (file)
Binary files a/theme/boost/amd/build/form-display-errors.min.js and b/theme/boost/amd/build/form-display-errors.min.js differ
index 4ff6e98..bfeb1b8 100644 (file)
Binary files a/theme/boost/amd/build/form-display-errors.min.js.map and b/theme/boost/amd/build/form-display-errors.min.js.map differ
index 230cbc5..a839184 100644 (file)
@@ -69,7 +69,7 @@ define(['jquery', 'core/event'], function($, Event) {
             });
 
             var form = element.closest('form');
-            if (!('boostFormErrorsEnhanced' in form.dataset)) {
+            if (form && !('boostFormErrorsEnhanced' in form.dataset)) {
                 form.addEventListener('submit', function() {
                     var visibleError = $('.form-control-feedback:visible');
                     if (visibleError.length) {
index 97e6983..a543505 100644 (file)
@@ -34,8 +34,6 @@ $string['bootswatch_desc'] = 'A bootswatch is a set of Bootstrap variables and c
 $string['choosereadme'] = 'Boost is a modern highly-customisable theme. This theme is intended to be used directly, or as a parent theme when creating new themes utilising Bootstrap 4.';
 $string['currentinparentheses'] = '(current)';
 $string['configtitle'] = 'Boost';
-$string['fontsize'] = 'Theme base fontsize';
-$string['fontsize_desc'] = 'Enter a fontsize in %';
 $string['generalsettings'] = 'General settings';
 $string['nobootswatch'] = 'None';
 $string['pluginname'] = 'Boost';
index 68a20f8..5a7ac95 100644 (file)
@@ -150,9 +150,5 @@ function theme_boost_get_pre_scss($theme) {
         $scss .= $theme->settings->scsspre;
     }
 
-    if (!empty($theme->settings->fontsize)) {
-        $scss .= '$font-size-base: ' . (1 / 100 * $theme->settings->fontsize) . "rem !default;\n";
-    }
-
     return $scss;
 }
index 53fdb52..6de983f 100644 (file)
@@ -1,35 +1,64 @@
 /* calendar.less */
 
 // Calendar colour variables defined.
-$calendarEventCategoryColor: #d8bfd8 !default; // Pale purple.
+$calendarEventCategoryColor: #e0cbe0 !default; // Pale purple.
 $calendarEventCourseColor: #ffd3bd !default; // Pale red.
 $calendarEventGlobalColor: #d6f8cd !default; // Pale green.
 $calendarEventGroupColor: #fee7ae !default; // Pale yellow.
 $calendarEventUserColor: #dce7ec !default; // Pale blue.
 $calendarEventOtherColor: #ced4da !default; // Pale gray.
 
+// Border colours for the event colour indicators.
+$calendarEventCategoryBorder: 2px solid #9e619f !default; // Purple.
+$calendarEventCourseBorder: 2px solid #d34600 !default; // Red-orange.
+$calendarEventGlobalBorder: 2px solid #2b8713 !default; // Green.
+$calendarEventGroupBorder: 2px solid #9a6e02 !default; // Dark orange.
+$calendarEventUserBorder: 2px solid #4e7c91 !default; // Blue.
+$calendarEventOtherBorder: 2px solid #687889 !default; // Gray.
+
+// This will be the colour of mini-calendar links, hide/show filter icons, edit/delete icon buttons.
+$calendarEventColor: #0d5ca1 !default;
+
 // Calendar event background colours defined.
 .calendar_event_category {
     background-color: $calendarEventCategoryColor;
+    .commands a {
+        color: $calendarEventColor;
+    }
 }
 .calendar_event_course {
     background-color: $calendarEventCourseColor;
+    .commands a {
+        color: $calendarEventColor;
+    }
 }
 
 .calendar_event_site {
     background-color: $calendarEventGlobalColor;
+    .commands a {
+        color: $calendarEventColor;
+    }
 }
 
 .calendar_event_group {
     background-color: $calendarEventGroupColor;
+    .commands a {
+        color: $calendarEventColor;
+    }
 }
 
 .calendar_event_user {
     background-color: $calendarEventUserColor;
+    .commands a {
+        color: $calendarEventColor;
+    }
 }
 
 .calendar_event_other {
     background-color: $calendarEventOtherColor;
+    .commands a {
+        color: $calendarEventColor;
+    }
 }
 
 // Calendar restyling.
@@ -139,25 +168,31 @@ $calendarEventOtherColor: #ced4da !default; // Pale gray.
 
                             &.calendar_event_category {
                                 background-color: $calendarEventCategoryColor;
+                                border: $calendarEventCategoryBorder;
                             }
                             &.calendar_event_course {
                                 background-color: $calendarEventCourseColor;
+                                border: $calendarEventCourseBorder;
                             }
 
                             &.calendar_event_site {
                                 background-color: $calendarEventGlobalColor;
+                                border: $calendarEventGlobalBorder;
                             }
 
                             &.calendar_event_group {
                                 background-color: $calendarEventGroupColor;
+                                border: $calendarEventGroupBorder;
                             }
 
                             &.calendar_event_user {
                                 background-color: $calendarEventUserColor;
+                                border: $calendarEventUserBorder;
                             }
 
                             &.calendar_event_other {
                                 background-color: $calendarEventOtherColor;
+                                border: $calendarEventOtherBorder;
                             }
                         }
                     }
@@ -289,6 +324,7 @@ $calendarEventOtherColor: #ced4da !default; // Pale gray.
                 width: 100%;
                 height: 100%;
                 display: block;
+                color: $calendarEventColor;
             }
 
             &.duration_global {
@@ -389,6 +425,36 @@ $calendarEventOtherColor: #ced4da !default; // Pale gray.
             margin-bottom: 0.2em;
 
             span {
+                &.calendar_event_category {
+                    i {
+                        color: $calendarEventColor;
+                    }
+                }
+                &.calendar_event_course {
+                    i {
+                        color: $calendarEventColor;
+                    }
+                }
+                &.calendar_event_site {
+                    i {
+                        color: $calendarEventColor;
+                    }
+                }
+                &.calendar_event_group {
+                    i {
+                        color: $calendarEventColor;
+                    }
+                }
+                &.calendar_event_user {
+                    i {
+                        color: $calendarEventColor;
+                    }
+                }
+                &.calendar_event_other {
+                    i {
+                        color: $calendarEventColor;
+                    }
+                }
                 img {
                     padding: 0 0.2em;
                     margin: 0;
index 1349003..7f73cd5 100644 (file)
@@ -537,13 +537,15 @@ span.editinstructions {
     margin-left: 30px;
     font-size: $font-size-sm;
     padding: .1em .4em;
-    background-color: $state-info-bg;
-    color: $state-info-text;
     text-decoration: none;
     z-index: 9999;
+    border: $alert-border-width solid transparent;
 
-    @include box-shadow(2px 2px 5px 1px #ccc);
-    border: 1px solid $state-info-border;
+    @include alert-variant(
+        theme-color-level('info', $alert-bg-level),
+        theme-color-level('info', $alert-border-level),
+        theme-color-level('info', $alert-color-level)
+    );
 }
 
 /* Course drag and drop upload styles */
@@ -553,14 +555,15 @@ span.editinstructions {
     width: 40%;
     margin: 0 30%;
     padding: 6px;
-    border: 1px solid $state-info-border;
     text-align: center;
-    background: $state-info-bg;
-    color: $state-info-text;
     z-index: 1; // Required in order to have this above relatively positioned course content@mixin
+    border: $alert-border-width solid transparent;
 
-    @include box-shadow(2px 2px 5px 1px #ccc);
-    @include border-radius(8px);
+    @include alert-variant(
+        theme-color-level('info', $alert-bg-level),
+        theme-color-level('info', $alert-border-level),
+        theme-color-level('info', $alert-color-level)
+    );
 }
 
 .dndupload-preview {
index 9a1735e..c08b5b0 100644 (file)
@@ -12721,22 +12721,34 @@ body.h5p-embed .h5pmessages {
 
 /* calendar.less */
 .calendar_event_category {
-  background-color: #d8bfd8; }
+  background-color: #e0cbe0; }
+  .calendar_event_category .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_course {
   background-color: #ffd3bd; }
+  .calendar_event_course .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_site {
   background-color: #d6f8cd; }
+  .calendar_event_site .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_group {
   background-color: #fee7ae; }
+  .calendar_event_group .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_user {
   background-color: #dce7ec; }
+  .calendar_event_user .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_other {
   background-color: #ced4da; }
+  .calendar_event_other .commands a {
+    color: #0d5ca1; }
 
 .path-calendar .calendartable {
   width: 100%;
@@ -12813,17 +12825,23 @@ body.h5p-embed .h5pmessages {
           border-radius: 6px;
           vertical-align: middle; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_category {
-            background-color: #d8bfd8; }
+            background-color: #e0cbe0;
+            border: 2px solid #9e619f; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_course {
-            background-color: #ffd3bd; }
+            background-color: #ffd3bd;
+            border: 2px solid #d34600; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_site {
-            background-color: #d6f8cd; }
+            background-color: #d6f8cd;
+            border: 2px solid #2b8713; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_group {
-            background-color: #fee7ae; }
+            background-color: #fee7ae;
+            border: 2px solid #9a6e02; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_user {
-            background-color: #dce7ec; }
+            background-color: #dce7ec;
+            border: 2px solid #4e7c91; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_other {
-            background-color: #ced4da; }
+            background-color: #ced4da;
+            border: 2px solid #687889; }
     .path-calendar .maincalendar .calendarmonth td {
       height: 5em; }
     .path-calendar .maincalendar .calendarmonth .clickable:hover {
@@ -12844,7 +12862,7 @@ body.h5p-embed .h5pmessages {
     color: #0b4f8a;
     text-decoration: underline; }
   .path-calendar .maincalendar .calendar_event_category {
-    border-color: #d8bfd8; }
+    border-color: #e0cbe0; }
   .path-calendar .maincalendar .calendar_event_course {
     border-color: #ffd3bd; }
   .path-calendar .maincalendar .calendar_event_site {
@@ -12887,17 +12905,18 @@ body.h5p-embed .h5pmessages {
   .block .minicalendar td a {
     width: 100%;
     height: 100%;
-    display: block; }
+    display: block;
+    color: #0d5ca1; }
   .block .minicalendar td.duration_global {
     border-top: 1px solid #d6f8cd;
     border-bottom: 1px solid #d6f8cd; }
     .block .minicalendar td.duration_global.duration_finish {
       background-color: #d6f8cd; }
   .block .minicalendar td.duration_category {
-    border-top: 1px solid #d8bfd8;
-    border-bottom: 1px solid #d8bfd8; }
+    border-top: 1px solid #e0cbe0;
+    border-bottom: 1px solid #e0cbe0; }
     .block .minicalendar td.duration_category.duration_finish {
-      background-color: #d8bfd8; }
+      background-color: #e0cbe0; }
   .block .minicalendar td.duration_course {
     border-top: 1px solid #ffd3bd;
     border-bottom: 1px solid #ffd3bd; }
@@ -12949,6 +12968,18 @@ body.h5p-embed .h5pmessages {
 
 .block .calendar_filters li {
   margin-bottom: 0.2em; }
+  .block .calendar_filters li span.calendar_event_category i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_course i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_site i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_group i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_user i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_other i {
+    color: #0d5ca1; }
   .block .calendar_filters li span img {
     padding: 0 0.2em;
     margin: 0; }
@@ -13490,11 +13521,16 @@ span.editinstructions {
   margin-left: 30px;
   font-size: 0.8203125rem;
   padding: .1em .4em;
-  background-color: #cce6ea;
-  color: #008196;
   text-decoration: none;
   z-index: 9999;
-  border: 1px solid #b8dce2; }
+  border: 0 solid transparent;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2; }
+  span.editinstructions hr {
+    border-top-color: #a6d3db; }
+  span.editinstructions .alert-link {
+    color: #00171b; }
 
 /* Course drag and drop upload styles */
 #dndupload-status {
@@ -13503,11 +13539,16 @@ span.editinstructions {
   width: 40%;
   margin: 0 30%;
   padding: 6px;
-  border: 1px solid #b8dce2;
   text-align: center;
-  background: #cce6ea;
-  color: #008196;
-  z-index: 1; }
+  z-index: 1;
+  border: 0 solid transparent;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2; }
+  #dndupload-status hr {
+    border-top-color: #a6d3db; }
+  #dndupload-status .alert-link {
+    color: #00171b; }
 
 .dndupload-preview {
   color: #909090;
index de04da2..903a2e1 100644 (file)
@@ -12936,22 +12936,34 @@ body.h5p-embed .h5pmessages {
 
 /* calendar.less */
 .calendar_event_category {
-  background-color: #d8bfd8; }
+  background-color: #e0cbe0; }
+  .calendar_event_category .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_course {
   background-color: #ffd3bd; }
+  .calendar_event_course .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_site {
   background-color: #d6f8cd; }
+  .calendar_event_site .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_group {
   background-color: #fee7ae; }
+  .calendar_event_group .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_user {
   background-color: #dce7ec; }
+  .calendar_event_user .commands a {
+    color: #0d5ca1; }
 
 .calendar_event_other {
   background-color: #ced4da; }
+  .calendar_event_other .commands a {
+    color: #0d5ca1; }
 
 .path-calendar .calendartable {
   width: 100%;
@@ -13028,17 +13040,23 @@ body.h5p-embed .h5pmessages {
           border-radius: 6px;
           vertical-align: middle; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_category {
-            background-color: #d8bfd8; }
+            background-color: #e0cbe0;
+            border: 2px solid #9e619f; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_course {
-            background-color: #ffd3bd; }
+            background-color: #ffd3bd;
+            border: 2px solid #d34600; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_site {
-            background-color: #d6f8cd; }
+            background-color: #d6f8cd;
+            border: 2px solid #2b8713; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_group {
-            background-color: #fee7ae; }
+            background-color: #fee7ae;
+            border: 2px solid #9a6e02; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_user {
-            background-color: #dce7ec; }
+            background-color: #dce7ec;
+            border: 2px solid #4e7c91; }
           .path-calendar .maincalendar .calendarmonth ul li .badge.badge-circle.calendar_event_other {
-            background-color: #ced4da; }
+            background-color: #ced4da;
+            border: 2px solid #687889; }
     .path-calendar .maincalendar .calendarmonth td {
       height: 5em; }
     .path-calendar .maincalendar .calendarmonth .clickable:hover {
@@ -13059,7 +13077,7 @@ body.h5p-embed .h5pmessages {
     color: #0b4f8a;
     text-decoration: underline; }
   .path-calendar .maincalendar .calendar_event_category {
-    border-color: #d8bfd8; }
+    border-color: #e0cbe0; }
   .path-calendar .maincalendar .calendar_event_course {
     border-color: #ffd3bd; }
   .path-calendar .maincalendar .calendar_event_site {
@@ -13102,17 +13120,18 @@ body.h5p-embed .h5pmessages {
   .block .minicalendar td a {
     width: 100%;
     height: 100%;
-    display: block; }
+    display: block;
+    color: #0d5ca1; }
   .block .minicalendar td.duration_global {
     border-top: 1px solid #d6f8cd;
     border-bottom: 1px solid #d6f8cd; }
     .block .minicalendar td.duration_global.duration_finish {
       background-color: #d6f8cd; }
   .block .minicalendar td.duration_category {
-    border-top: 1px solid #d8bfd8;
-    border-bottom: 1px solid #d8bfd8; }
+    border-top: 1px solid #e0cbe0;
+    border-bottom: 1px solid #e0cbe0; }
     .block .minicalendar td.duration_category.duration_finish {
-      background-color: #d8bfd8; }
+      background-color: #e0cbe0; }
   .block .minicalendar td.duration_course {
     border-top: 1px solid #ffd3bd;
     border-bottom: 1px solid #ffd3bd; }
@@ -13164,6 +13183,18 @@ body.h5p-embed .h5pmessages {
 
 .block .calendar_filters li {
   margin-bottom: 0.2em; }
+  .block .calendar_filters li span.calendar_event_category i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_course i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_site i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_group i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_user i {
+    color: #0d5ca1; }
+  .block .calendar_filters li span.calendar_event_other i {
+    color: #0d5ca1; }
   .block .calendar_filters li span img {
     padding: 0 0.2em;
     margin: 0; }
@@ -13705,11 +13736,16 @@ span.editinstructions {
   margin-left: 30px;
   font-size: 0.8203125rem;
   padding: .1em .4em;
-  background-color: #cce6ea;
-  color: #008196;
   text-decoration: none;
   z-index: 9999;
-  border: 1px solid #b8dce2; }
+  border: 0 solid transparent;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2; }
+  span.editinstructions hr {
+    border-top-color: #a6d3db; }
+  span.editinstructions .alert-link {
+    color: #00171b; }
 
 /* Course drag and drop upload styles */
 #dndupload-status {
@@ -13718,12 +13754,16 @@ span.editinstructions {
   width: 40%;
   margin: 0 30%;
   padding: 6px;
-  border: 1px solid #b8dce2;
   text-align: center;
-  background: #cce6ea;
-  color: #008196;
   z-index: 1;
-  border-radius: 8px; }
+  border: 0 solid transparent;
+  color: #00434e;
+  background-color: #cce6ea;
+  border-color: #b8dce2; }
+  #dndupload-status hr {
+    border-top-color: #a6d3db; }
+  #dndupload-status .alert-link {
+    color: #00171b; }
 
 .dndupload-preview {
   color: #909090;