Merge branch 'wip-MDL-52100-master' of git://github.com/abgreeve/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 18 Dec 2017 09:07:46 +0000 (10:07 +0100)
committerDavid Monllao <davidm@moodle.com>
Mon, 18 Dec 2017 09:07:46 +0000 (10:07 +0100)
25 files changed:
auth/mnet/auth.php
backup/cc/entity11.resource.class.php
blocks/course_list/block_course_list.php
blocks/myoverview/classes/output/main.php
calendar/externallib.php
course/edit_form.php
course/renderer.php
enrol/tests/enrollib_test.php
enrol/upgrade.txt
lib/enrollib.php
lib/filestorage/stored_file.php
lib/myprofilelib.php
lib/navigationlib.php
mod/assign/locallib.php
mod/assign/tests/locallib_test.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/styles.css
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/tests/lib_test.php

index 1f269f7..b3b22f5 100644 (file)
@@ -382,7 +382,7 @@ class auth_plugin_mnet extends auth_plugin_base {
             // with info so that the IDP can maintain mnetservice_enrol_enrolments
             $mnetrequest->add_param($remoteuser->username);
             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
-            $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
+            $courses = enrol_get_users_courses($localuser->id, false, $fields);
             if (is_array($courses) && !empty($courses)) {
                 // Second request to do the JOINs that we'd have done
                 // inside enrol_get_users_courses() if we had been allowed
index bc5bb5a..7bb6da7 100644 (file)
@@ -99,7 +99,7 @@ class cc11_resource extends entities11 {
                                 $link = 'http://invalidurldetected/';
                             }
                         } else {
-                            $link = $rawlink;
+                            $link = htmlspecialchars(trim($rawlink), ENT_COMPAT, 'UTF-8', false);
                         }
                     }
                 }
index 36003f7..10cbf23 100644 (file)
@@ -57,14 +57,7 @@ class block_course_list extends block_list {
 
         if (empty($CFG->disablemycourses) and isloggedin() and !isguestuser() and
           !(has_capability('moodle/course:update', context_system::instance()) and $adminseesall)) {    // Just print My Courses
-            // As this is producing navigation sort order should default to $CFG->navsortmycoursessort instead
-            // of using the default.
-            if (!empty($CFG->navsortmycoursessort)) {
-                $sortorder = 'visible DESC, ' . $CFG->navsortmycoursessort . ' ASC';
-            } else {
-                $sortorder = 'visible DESC, sortorder ASC';
-            }
-            if ($courses = enrol_get_my_courses(NULL, $sortorder)) {
+            if ($courses = enrol_get_my_courses()) {
                 foreach ($courses as $course) {
                     $coursecontext = context_course::instance($course->id);
                     $linkcss = $course->visible ? "" : " class=\"dimmed\" ";
index 2435f54..2850637 100644 (file)
@@ -63,7 +63,7 @@ class main implements renderable, templatable {
     public function export_for_template(renderer_base $output) {
         global $USER;
 
-        $courses = enrol_get_my_courses('*', 'fullname ASC');
+        $courses = enrol_get_my_courses('*');
         $coursesprogress = [];
 
         foreach ($courses as $course) {
index 049f13d..7592536 100644 (file)
@@ -503,7 +503,7 @@ class core_calendar_external extends external_api {
             $params['aftereventid'] = null;
         }
 
-        $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, [$courseid]);
+        $courses = enrol_get_my_courses('*', null, 0, [$courseid]);
         $courses = array_values($courses);
 
         if (empty($courses)) {
@@ -588,7 +588,7 @@ class core_calendar_external extends external_api {
         }
 
         $renderer = $PAGE->get_renderer('core_calendar');
-        $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, $params['courseids']);
+        $courses = enrol_get_my_courses('*', null, 0, $params['courseids']);
         $courses = array_values($courses);
 
         if (empty($courses)) {
index 4c03e2d..47c8c18 100644 (file)
@@ -88,7 +88,7 @@ class course_edit_form extends moodleform {
             }
         } else {
             if (has_capability('moodle/course:changecategory', $coursecontext)) {
-                $displaylist = coursecat::make_categories_list('moodle/course:create');
+                $displaylist = coursecat::make_categories_list('moodle/course:changecategory');
                 if (!isset($displaylist[$course->category])) {
                     //always keep current
                     $displaylist[$course->category] = coursecat::get($course->category, MUST_EXIST, true)->get_formatted_name();
index 64b2575..d759837 100644 (file)
@@ -1988,13 +1988,7 @@ class core_course_renderer extends plugin_renderer_base {
         }
 
         $output = '';
-        if (!empty($CFG->navsortmycoursessort)) {
-            // sort courses the same as in navigation menu
-            $sortorder = 'visible DESC,'. $CFG->navsortmycoursessort.' ASC';
-        } else {
-            $sortorder = 'visible DESC,sortorder ASC';
-        }
-        $courses  = enrol_get_my_courses('summary, summaryformat', $sortorder);
+        $courses  = enrol_get_my_courses('summary, summaryformat');
         $rhosts   = array();
         $rcourses = array();
         if (!empty($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode==='strict') {
index d52dc6f..1678462 100644 (file)
@@ -55,10 +55,24 @@ class core_enrollib_testcase extends advanced_testcase {
 
         $category1 = $this->getDataGenerator()->create_category(array('visible'=>0));
         $category2 = $this->getDataGenerator()->create_category();
-        $course1 = $this->getDataGenerator()->create_course(array('category'=>$category1->id));
-        $course2 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
-        $course3 = $this->getDataGenerator()->create_course(array('category'=>$category2->id, 'visible'=>0));
-        $course4 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
+
+        $course1 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'Z',
+            'category' => $category1->id,
+        ));
+        $course2 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'X',
+            'category' => $category2->id,
+        ));
+        $course3 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'Y',
+            'category' => $category2->id,
+            'visible' => 0,
+        ));
+        $course4 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'W',
+            'category' => $category2->id,
+        ));
 
         $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
         $DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id'=>$maninstance1->id));
@@ -150,6 +164,18 @@ class core_enrollib_testcase extends advanced_testcase {
 
         $courses = enrol_get_all_users_courses($user2->id, false, null, 'id DESC');
         $this->assertEquals(array($course3->id, $course2->id, $course1->id), array_keys($courses));
+
+        // Make sure that implicit sorting defined in navsortmycoursessort is respected.
+
+        $CFG->navsortmycoursessort = 'shortname';
+
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
+
+        // But still the explicit sorting takes precedence over the implicit one.
+
+        $courses = enrol_get_all_users_courses($user1->id, false, null, 'shortname DESC');
+        $this->assertEquals(array($course1->id, $course3->id, $course2->id), array_keys($courses));
     }
 
     public function test_enrol_user_sees_own_courses() {
@@ -590,15 +616,15 @@ class core_enrollib_testcase extends advanced_testcase {
         // Create test user and 4 courses, two of which have guest access enabled.
         $user = $this->getDataGenerator()->create_user();
         $course1 = $this->getDataGenerator()->create_course(
-                (object)array('shortname' => 'Z',
+                (object)array('shortname' => 'X',
                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
                 'enrol_guest_password_0' => ''));
         $course2 = $this->getDataGenerator()->create_course(
-                (object)array('shortname' => 'Y',
+                (object)array('shortname' => 'Z',
                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
                 'enrol_guest_password_0' => ''));
         $course3 = $this->getDataGenerator()->create_course(
-                (object)array('shortname' => 'X',
+                (object)array('shortname' => 'Y',
                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
                 'enrol_guest_password_0' => 'frog'));
         $course4 = $this->getDataGenerator()->create_course(
@@ -645,10 +671,19 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertObjectHasAttribute('summary', $courses[$course3->id]);
         $this->assertObjectHasAttribute('summaryformat', $courses[$course3->id]);
 
-        // Check sort parameter still works.
-        $courses = enrol_get_my_courses(null, 'shortname', 0, [], true);
+        // By default, courses are ordered by sortorder - which by default is most recent first.
+        $courses = enrol_get_my_courses(null, null, 0, [], true);
         $this->assertEquals([$course3->id, $course2->id, $course1->id], array_keys($courses));
 
+        // Make sure that implicit sorting defined in navsortmycoursessort is respected.
+        $CFG->navsortmycoursessort = 'shortname';
+        $courses = enrol_get_my_courses(null, null, 0, [], true);
+        $this->assertEquals([$course1->id, $course3->id, $course2->id], array_keys($courses));
+
+        // But still the explicit sorting takes precedence over the implicit one.
+        $courses = enrol_get_my_courses(null, 'shortname DESC', 0, [], true);
+        $this->assertEquals([$course2->id, $course3->id, $course1->id], array_keys($courses));
+
         // Check filter parameter still works.
         $courses = enrol_get_my_courses(null, 'id', 0, [$course2->id, $course3->id, $course4->id], true);
         $this->assertEquals([$course2->id, $course3->id], array_keys($courses));
index 528fe37..09ff3ba 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /enrol/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.5 ===
+
+* Default sorting in enrol_get_my_courses(), enrol_get_all_users_courses() and enrol_get_users_courses() now respects
+  the site setting "navsortmycoursessort" and should be consistently used when displaying the courses in the UI.
+
 === 3.4 ===
 
 * render_course_enrolment_users_table method has been removed from the renderer. The enrolled users page is now
index d69094b..1a73b2d 100644 (file)
@@ -544,26 +544,32 @@ function enrol_add_course_navigation(navigation_node $coursenode, $course) {
 /**
  * Returns list of courses current $USER is enrolled in and can access
  *
- * - $fields is an array of field names to ADD
- *   so name the fields you really need, which will
- *   be added and uniq'd
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
  *
  * If $allaccessible is true, this will additionally return courses that the current user is not
  * enrolled in, but can access because they are open to the user for other reasons (course view
  * permission, currently viewing course as a guest, or course allows guest access without
  * password).
  *
- * @param string|array $fields
- * @param string $sort
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
  * @param int $limit max number of courses
  * @param array $courseids the list of course ids to filter by
  * @param bool $allaccessible Include courses user is not enrolled in, but can access
  * @return array
  */
-function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder ASC',
-          $limit = 0, $courseids = [], $allaccessible = false) {
+function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false) {
     global $DB, $USER, $CFG;
 
+    if ($sort === null) {
+        if (empty($CFG->navsortmycoursessort)) {
+            $sort = 'visible DESC, sortorder ASC';
+        } else {
+            $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
+        }
+    }
+
     // Guest account does not have any enrolled courses.
     if (!$allaccessible && (isguestuser() or !isloggedin())) {
         return array();
@@ -584,7 +590,7 @@ function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder AS
     } else if (is_array($fields)) {
         $fields = array_unique(array_merge($basefields, $fields));
     } else {
-        throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
+        throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
     }
     if (in_array('*', $fields)) {
         $fields = array('*');
@@ -788,19 +794,19 @@ function enrol_get_course_description_texts($course) {
 
 /**
  * Returns list of courses user is enrolled into.
- * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
  *
- * - $fields is an array of fieldnames to ADD
- *   so name the fields you really need, which will
- *   be added and uniq'd
+ * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
  *
- * @param int $userid
- * @param bool $onlyactive return only active enrolments in courses user may see
- * @param string|array $fields
- * @param string $sort
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
+ *
+ * @param int $userid User whose courses are returned, defaults to the current user.
+ * @param bool $onlyactive Return only active enrolments in courses user may see.
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
  * @return array
  */
-function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
+function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
     global $DB;
 
     $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
@@ -877,19 +883,27 @@ function enrol_user_sees_own_courses($user = null) {
 }
 
 /**
- * Returns list of courses user is enrolled into without any capability checks
- * - $fields is an array of fieldnames to ADD
- *   so name the fields you really need, which will
- *   be added and uniq'd
+ * Returns list of courses user is enrolled into without performing any capability checks.
  *
- * @param int $userid
- * @param bool $onlyactive return only active enrolments in courses user may see
- * @param string|array $fields
- * @param string $sort
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
+ *
+ * @param int $userid User whose courses are returned, defaults to the current user.
+ * @param bool $onlyactive Return only active enrolments in courses user may see.
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
  * @return array
  */
-function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
-    global $DB;
+function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
+    global $CFG, $DB;
+
+    if ($sort === null) {
+        if (empty($CFG->navsortmycoursessort)) {
+            $sort = 'visible DESC, sortorder ASC';
+        } else {
+            $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
+        }
+    }
 
     // Guest account does not have any courses
     if (isguestuser($userid) or empty($userid)) {
@@ -912,7 +926,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NUL
     } else if (is_array($fields)) {
         $fields = array_unique(array_merge($basefields, $fields));
     } else {
-        throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
+        throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
     }
     if (in_array('*', $fields)) {
         $fields = array('*');
index 36f186f..c1a4254 100644 (file)
@@ -304,7 +304,9 @@ class stored_file {
         $filerecord->filesize = $newfile->get_filesize();
         $filerecord->referencefileid = $newfile->get_referencefileid();
         $filerecord->userid = $newfile->get_userid();
+        $oldcontenthash = $this->get_contenthash();
         $this->update($filerecord);
+        $this->filesystem->remove_file($oldcontenthash);
     }
 
     /**
index c873280..5ce9104 100644 (file)
@@ -225,7 +225,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
 
     if (!isset($hiddenfields['mycourses'])) {
         $showallcourses = optional_param('showallcourses', 0, PARAM_INT);
-        if ($mycourses = enrol_get_all_users_courses($user->id, true, null, 'visible DESC, sortorder ASC')) {
+        if ($mycourses = enrol_get_all_users_courses($user->id, true, null)) {
             $shown = 0;
             $courselisting = html_writer::start_tag('ul');
             foreach ($mycourses as $mycourse) {
index 9f22f18..16d7b63 100644 (file)
@@ -2920,14 +2920,7 @@ class global_navigation extends navigation_node {
 
         $limit = (int) $CFG->navcourselimit;
 
-        $sortorder = 'visible DESC';
-        // Prevent undefined $CFG->navsortmycoursessort errors.
-        if (empty($CFG->navsortmycoursessort)) {
-            $CFG->navsortmycoursessort = 'sortorder';
-        }
-        // Append the chosen sortorder.
-        $sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC';
-        $courses = enrol_get_my_courses('*', $sortorder);
+        $courses = enrol_get_my_courses('*');
         $flatnavcourses = [];
 
         // Go through the courses and see which ones we want to display in the flatnav.
index ae24d0f..57981d5 100644 (file)
@@ -3774,7 +3774,7 @@ class assign {
         $grade = $this->get_user_grade($userid, false, $attemptnumber);
         $flags = $this->get_user_flags($userid, false);
         if ($this->can_view_submission($userid)) {
-            $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
+            $submissionlocked = ($flags && $flags->locked);
             $extensionduedate = null;
             if ($flags) {
                 $extensionduedate = $flags->extensionduedate;
@@ -3791,7 +3791,7 @@ class assign {
                                                                      $submissiongroup,
                                                                      $notsubmitted,
                                                                      $this->is_any_submission_plugin_enabled(),
-                                                                     $gradelocked,
+                                                                     $submissionlocked,
                                                                      $this->is_graded($userid),
                                                                      $instance->duedate,
                                                                      $instance->cutoffdate,
@@ -3972,7 +3972,7 @@ class assign {
         $grade = $this->get_user_grade($userid, false, $attemptnumber);
         $flags = $this->get_user_flags($userid, false);
         if ($this->can_view_submission($userid)) {
-            $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
+            $submissionlocked = ($flags && $flags->locked);
             $extensionduedate = null;
             if ($flags) {
                 $extensionduedate = $flags->extensionduedate;
@@ -3989,7 +3989,7 @@ class assign {
                                                              $submissiongroup,
                                                              $notsubmitted,
                                                              $this->is_any_submission_plugin_enabled(),
-                                                             $gradelocked,
+                                                             $submissionlocked,
                                                              $this->is_graded($userid),
                                                              $instance->duedate,
                                                              $instance->cutoffdate,
@@ -4946,7 +4946,7 @@ class assign {
                     ($this->is_any_submission_plugin_enabled()) &&
                     $this->can_edit_submission($user->id);
 
-        $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id, false);
+        $submissionlocked = ($flags && $flags->locked);
 
         // Grading criteria preview.
         $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
@@ -4977,7 +4977,7 @@ class assign {
                                                           $submissiongroup,
                                                           $notsubmitted,
                                                           $this->is_any_submission_plugin_enabled(),
-                                                          $gradelocked,
+                                                          $submissionlocked,
                                                           $this->is_graded($user->id),
                                                           $instance->duedate,
                                                           $instance->cutoffdate,
index 43391b6..e5adc30 100644 (file)
@@ -3096,4 +3096,51 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         // Check that the grade was updated in the gradebook by fix_null_grades.
         $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
     }
+
+    /**
+     * Test grade override displays 'Graded' for students
+     */
+    public function test_grade_submission_override() {
+        global $DB, $PAGE, $OUTPUT;
+
+        $this->setUser($this->editingteachers[0]);
+        $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1));
+
+        $studentid = $this->students[0]->id;
+
+        // Simulate adding a grade.
+        $this->setUser($this->teachers[0]);
+        $data = new stdClass();
+        $data->grade = '50.0';
+        $assign->testable_apply_grade_to_user($data, $studentid, 0);
+
+        // Set grade override.
+        $gradegrade = grade_grade::fetch(array('userid' => $studentid, 'itemid' => $assign->get_grade_item()->id));
+
+        // Check that grade submission is not overridden yet.
+        $this->assertEquals(false, $gradegrade->is_overridden());
+
+        // Simulate a submission.
+        $this->setUser($this->students[0]);
+        $submission = $assign->get_user_submission($studentid, true);
+
+        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)));
+
+        // Set override grade grade, and check that grade submission has been overridden.
+        $gradegrade->set_overridden(true);
+        $this->assertEquals(true, $gradegrade->is_overridden());
+
+        // Check that submissionslocked message 'This assignment is not accepting submissions' does not appear for student.
+        $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
+        $output = $assign->get_renderer()->render($gradingtable);
+        $this->assertContains(get_string('submissionstatus_', 'assign'), $output);
+
+        $assignsubmissionstatus = $assign->get_assign_submission_status_renderable($this->students[0], true);
+        $output2 = $assign->get_renderer()->render($assignsubmissionstatus);
+
+        // Check that submissionslocked 'This assignment is not accepting submissions' message does not appear for student.
+        $this->assertNotContains(get_string('submissionslocked', 'assign'), $output2);
+        // Check that submissionstatus_marked 'Graded' message does appear for student.
+        $this->assertContains(get_string('submissionstatus_marked', 'assign'), $output2);
+    }
 }
index 238a1a8..e738392 100644 (file)
@@ -443,11 +443,12 @@ class edit_renderer extends \plugin_renderer_base {
             $help = '';
         }
 
+        $helpspan = html_writer::span($help, 'shuffle-help-tip');
         $progressspan = html_writer::span('', 'shuffle-progress');
         $checkbox = html_writer::empty_tag('input', $checkboxattributes);
-        $label = html_writer::label(get_string('shufflequestions', 'quiz') . ' ' . $help,
+        $label = html_writer::label(get_string('shufflequestions', 'quiz'),
                 $checkboxattributes['id'], false);
-        return html_writer::span($progressspan . $checkbox . $label,
+        return html_writer::span($progressspan . $checkbox . $label. ' ' . $helpspan,
                 'instanceshufflequestions', array('data-action' => 'shuffle_questions'));
     }
 
index 6e85f99..ee746fe 100644 (file)
@@ -739,6 +739,11 @@ table.quizreviewsummary td.cell {
     margin: 0.3em 20px 0 0;
 }
 
+.instanceshufflequestions [type="checkbox"] {
+    vertical-align: middle;
+    margin-right: .5rem;
+}
+
 #page-mod-quiz-edit ul.section {
     margin: 0;
     padding: 0 20px;
index e1af1d1..76043c7 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js differ
index 4bbeaf8..1c832fd 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js differ
index e1af1d1..76043c7 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js differ
index 5d46deb..78c4ae9 100644 (file)
@@ -50,7 +50,7 @@ var CSS = {
         EDITSECTION: 'a.editing_section',
         EDITSECTIONICON: 'a.editing_section .icon',
         EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]',
-        EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress',
+        EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress .shuffle-help-tip',
         HIDE: 'a.editing_hide',
         HIGHLIGHT: 'a.editing_highlight',
         INSTANCENAME: 'span.instancename',
index 06c2e1c..c6ad22f 100644 (file)
@@ -297,6 +297,7 @@ $string['notattempted'] = 'Not attempted';
 $string['not_corr_type'] = 'Type mismatch for tag {$a->tag}';
 $string['notopenyet'] = 'Sorry, this activity is not available until {$a}';
 $string['objectives'] = 'Objectives';
+$string['openafterclose'] = 'You have specified an open date after the close date';
 $string['optallstudents'] = 'all users';
 $string['optattemptsonly'] = 'users with attempts only';
 $string['optnoattemptsonly'] = 'users with no attempts only';
index 2416be8..622b994 100644 (file)
@@ -50,6 +50,9 @@ define('SCORM_DISPLAY_ATTEMPTSTATUS_ALL', 1);
 define('SCORM_DISPLAY_ATTEMPTSTATUS_MY', 2);
 define('SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY', 3);
 
+define('SCORM_EVENT_TYPE_OPEN', 'open');
+define('SCORM_EVENT_TYPE_CLOSE', 'close');
+
 /**
  * Return an array of status options
  *
@@ -1798,3 +1801,117 @@ function mod_scorm_get_completion_active_rule_descriptions($cm) {
     }
     return $descriptions;
 }
+
+/**
+ * This function will update the scorm module according to the
+ * event that has been modified.
+ *
+ * It will set the timeopen or timeclose value of the scorm instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @param stdClass $scorm The module instance to get the range from
+ */
+function mod_scorm_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $scorm) {
+    global $DB;
+
+    if (empty($event->instance) || $event->modulename != 'scorm') {
+        return;
+    }
+
+    if ($event->instance != $scorm->id) {
+        return;
+    }
+
+    if (!in_array($event->eventtype, [SCORM_EVENT_TYPE_OPEN, SCORM_EVENT_TYPE_CLOSE])) {
+        return;
+    }
+
+    $courseid = $event->courseid;
+    $modulename = $event->modulename;
+    $instanceid = $event->instance;
+    $modified = false;
+
+    $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
+    $context = context_module::instance($coursemodule->id);
+
+    // The user does not have the capability to modify this activity.
+    if (!has_capability('moodle/course:manageactivities', $context)) {
+        return;
+    }
+
+    if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) {
+        // If the event is for the scorm activity opening then we should
+        // set the start time of the scorm activity to be the new start
+        // time of the event.
+        if ($scorm->timeopen != $event->timestart) {
+            $scorm->timeopen = $event->timestart;
+            $scorm->timemodified = time();
+            $modified = true;
+        }
+    } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) {
+        // If the event is for the scorm activity closing then we should
+        // set the end time of the scorm activity to be the new start
+        // time of the event.
+        if ($scorm->timeclose != $event->timestart) {
+            $scorm->timeclose = $event->timestart;
+            $modified = true;
+        }
+    }
+
+    if ($modified) {
+        $scorm->timemodified = time();
+        $DB->update_record('scorm', $scorm);
+        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+        $event->trigger();
+    }
+}
+
+/**
+ * This function calculates the minimum and maximum cutoff values for the timestart of
+ * the given event.
+ *
+ * It will return an array with two values, the first being the minimum cutoff value and
+ * the second being the maximum cutoff value. Either or both values can be null, which
+ * indicates there is no minimum or maximum, respectively.
+ *
+ * If a cutoff is required then the function must return an array containing the cutoff
+ * timestamp and error string to display to the user if the cutoff value is violated.
+ *
+ * A minimum and maximum cutoff return value will look like:
+ * [
+ *     [1505704373, 'The date must be after this date'],
+ *     [1506741172, 'The date must be before this date']
+ * ]
+ *
+ * @param \calendar_event $event The calendar event to get the time range for
+ * @param \stdClass $instance The module instance to get the range from
+ * @return array Returns an array with min and max date.
+ */
+function mod_scorm_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
+    $mindate = null;
+    $maxdate = null;
+
+    if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) {
+        // The start time of the open event can't be equal to or after the
+        // close time of the scorm activity.
+        if (!empty($instance->timeclose)) {
+            $maxdate = [
+                $instance->timeclose,
+                get_string('openafterclose', 'scorm')
+            ];
+        }
+    } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) {
+        // The start time of the close event can't be equal to or earlier than the
+        // open time of the scorm activity.
+        if (!empty($instance->timeopen)) {
+            $mindate = [
+                $instance->timeopen,
+                get_string('closebeforeopen', 'scorm')
+            ];
+        }
+    }
+
+    return [$mindate, $maxdate];
+}
\ No newline at end of file
index 869de92..58fbc8a 100644 (file)
@@ -51,9 +51,6 @@ define('LASTATTEMPT', '3');
 define('TOCJSLINK', 1);
 define('TOCFULLURL', 2);
 
-define('SCORM_EVENT_TYPE_OPEN', 'open');
-define('SCORM_EVENT_TYPE_CLOSE', 'close');
-
 // Local Library of functions for module scorm.
 
 /**
index 9440e24..0e98e3b 100644 (file)
@@ -360,9 +360,10 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
      * @param int $courseid
      * @param int $instanceid The data id.
      * @param string $eventtype The event type. eg. DATA_EVENT_TYPE_OPEN.
+     * @param int|null $timestart The start timestamp for the event
      * @return bool|calendar_event
      */
-    private function create_action_event($courseid, $instanceid, $eventtype) {
+    private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) {
         $event = new stdClass();
         $event->name = 'Calendar event';
         $event->modulename = 'scorm';
@@ -370,7 +371,13 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         $event->instance = $instanceid;
         $event->type = CALENDAR_EVENT_TYPE_ACTION;
         $event->eventtype = $eventtype;
-        $event->timestart = time();
+        $event->eventtype = $eventtype;
+
+        if ($timestart) {
+            $event->timestart = $timestart;
+        } else {
+            $event->timestart = time();
+        }
 
         return calendar_event::create($event);
     }
@@ -433,4 +440,300 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(mod_scorm_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_scorm_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * An unkown event type should not change the scorm instance.
+     */
+    public function test_mod_scorm_core_calendar_event_timestart_updated_unknown_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $scormgenerator = $generator->get_plugin_generator('mod_scorm');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $scorm = $scormgenerator->create_instance(['course' => $course->id]);
+        $scorm->timeopen = $timeopen;
+        $scorm->timeclose = $timeclose;
+        $DB->update_record('scorm', $scorm);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'scorm',
+            'instance' => $scorm->id,
+            'eventtype' => SCORM_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_scorm_core_calendar_event_timestart_updated($event, $scorm);
+
+        $scorm = $DB->get_record('scorm', ['id' => $scorm->id]);
+        $this->assertEquals($timeopen, $scorm->timeopen);
+        $this->assertEquals($timeclose, $scorm->timeclose);
+    }
+
+    /**
+     * A SCORM_EVENT_TYPE_OPEN event should update the timeopen property of
+     * the scorm activity.
+     */
+    public function test_mod_scorm_core_calendar_event_timestart_updated_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $scormgenerator = $generator->get_plugin_generator('mod_scorm');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeopen = $timeopen - DAYSECS;
+        $scorm = $scormgenerator->create_instance(['course' => $course->id]);
+        $scorm->timeopen = $timeopen;
+        $scorm->timeclose = $timeclose;
+        $scorm->timemodified = $timemodified;
+        $DB->update_record('scorm', $scorm);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'scorm',
+            'instance' => $scorm->id,
+            'eventtype' => SCORM_EVENT_TYPE_OPEN,
+            'timestart' => $newtimeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+
+        mod_scorm_core_calendar_event_timestart_updated($event, $scorm);
+
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+
+        $scorm = $DB->get_record('scorm', ['id' => $scorm->id]);
+        // Ensure the timeopen property matches the event timestart.
+        $this->assertEquals($newtimeopen, $scorm->timeopen);
+        // Ensure the timeclose isn't changed.
+        $this->assertEquals($timeclose, $scorm->timeclose);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $scorm->timemodified);
+        // Confirm that a module updated event is fired when the module
+        // is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * A SCORM_EVENT_TYPE_CLOSE event should update the timeclose property of
+     * the scorm activity.
+     */
+    public function test_mod_scorm_core_calendar_event_timestart_updated_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $scormgenerator = $generator->get_plugin_generator('mod_scorm');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $scorm = $scormgenerator->create_instance(['course' => $course->id]);
+        $scorm->timeopen = $timeopen;
+        $scorm->timeclose = $timeclose;
+        $scorm->timemodified = $timemodified;
+        $DB->update_record('scorm', $scorm);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'scorm',
+            'instance' => $scorm->id,
+            'eventtype' => SCORM_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+
+        mod_scorm_core_calendar_event_timestart_updated($event, $scorm);
+
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+
+        $scorm = $DB->get_record('scorm', ['id' => $scorm->id]);
+        // Ensure the timeclose property matches the event timestart.
+        $this->assertEquals($newtimeclose, $scorm->timeclose);
+        // Ensure the timeopen isn't changed.
+        $this->assertEquals($timeopen, $scorm->timeopen);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $scorm->timemodified);
+        // Confirm that a module updated event is fired when the module
+        // is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * An unkown event type should not have any limits
+     */
+    public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_unknown_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $scorm = new \stdClass();
+        $scorm->timeopen = $timeopen;
+        $scorm->timeclose = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'scorm',
+            'instance' => 1,
+            'eventtype' => SCORM_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The open event should be limited by the scorm's timeclose property, if it's set.
+     */
+    public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $scorm = new \stdClass();
+        $scorm->timeopen = $timeopen;
+        $scorm->timeclose = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'scorm',
+            'instance' => 1,
+            'eventtype' => SCORM_EVENT_TYPE_OPEN,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
+
+        $this->assertNull($min);
+        $this->assertEquals($timeclose, $max[0]);
+
+        // No timeclose value should result in no upper limit.
+        $scorm->timeclose = 0;
+        list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
+
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The close event should be limited by the scorm's timeopen property, if it's set.
+     */
+    public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $scorm = new \stdClass();
+        $scorm->timeopen = $timeopen;
+        $scorm->timeclose = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'scorm',
+            'instance' => 1,
+            'eventtype' => SCORM_EVENT_TYPE_CLOSE,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
+
+        $this->assertEquals($timeopen, $min[0]);
+        $this->assertNull($max);
+
+        // No timeclose value should result in no upper limit.
+        $scorm->timeopen = 0;
+        list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
+
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
 }