Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorDavid Monllao <davidm@moodle.com>
Wed, 8 Nov 2017 08:47:38 +0000 (09:47 +0100)
committerDavid Monllao <davidm@moodle.com>
Wed, 8 Nov 2017 08:47:38 +0000 (09:47 +0100)
138 files changed:
.travis.yml
admin/tool/analytics/cli/guess_course_start_and_end.php
admin/tool/phpunit/cli/util.php
analytics/classes/course.php
analytics/classes/local/indicator/community_of_inquiry_activity.php
analytics/tests/community_of_inquiry_activities_completed_by.php [moved from analytics/tests/course_activities_test.php with 52% similarity]
auth/ldap/lang/en/auth_ldap.php
blocks/html/block_html.php
calendar/amd/build/view_manager.min.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/local/api.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/forms/eventtype.php [new file with mode: 0644]
calendar/classes/local/event/forms/managesubscriptions.php [moved from calendar/managesubscriptions_form.php with 80% similarity]
calendar/externallib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/renderer.php
calendar/templates/event_summary_body.mustache
calendar/templates/month_detailed.mustache
calendar/tests/behat/calendar.feature
calendar/tests/behat/calendar_import.feature
calendar/tests/events_test.php
calendar/tests/externallib_test.php
calendar/tests/lib_test.php
calendar/tests/local_api_test.php
calendar/upgrade.txt
cohort/externallib.php
composer.json
composer.lock
course/classes/analytics/indicator/completion_enabled.php
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/src/quickenrolment.js
enrol/manual/classes/enrol_users_form.php
enrol/tests/behat/behat_enrol.php
lang/en/deprecated.txt
lang/en/message.php
lang/en/moodle.php
lang/en/notes.php
lang/en/role.php
lib/amd/build/auto_rows.min.js
lib/amd/build/modal_factory.min.js
lib/amd/src/auto_rows.js
lib/amd/src/modal_factory.js
lib/bennu/iCalendar_properties.php
lib/bennu/readme_moodle.txt
lib/classes/event/calendar_subscription_created.php
lib/classes/event/calendar_subscription_deleted.php
lib/classes/event/calendar_subscription_updated.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/filestorage/tests/file_system_filedir_test.php
lib/filestorage/tests/zip_packer_test.php
lib/mlbackend/python/classes/processor.php
lib/phpunit/bootstrap.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/arraydataset.php
lib/phpunit/classes/autoloader.php
lib/phpunit/classes/base_testcase.php
lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php
lib/phpunit/classes/hint_resultprinter.php
lib/phpunit/classes/util.php
lib/phpunit/phpunit.xsd
lib/phpunit/tests/advanced_test.php
lib/tests/event_test.php
lib/tests/moodlelib_test.php
lib/tests/session_redis_test.php
lib/tests/statslib_test.php
lib/weblib.php
mod/assign/classes/analytics/indicator/activity_base.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/lib_test.php
mod/chat/beep.mp3 [new file with mode: 0644]
mod/chat/beep.wav [deleted file]
mod/chat/chatd.php
mod/chat/gui_ajax/module.js
mod/chat/gui_header_js/jsupdate.php
mod/chat/gui_header_js/jsupdated.php
mod/choice/classes/analytics/indicator/activity_base.php
mod/choice/classes/analytics/indicator/cognitive_depth.php
mod/choice/classes/analytics/indicator/social_breadth.php
mod/choice/lib.php
mod/choice/tests/lib_test.php
mod/data/classes/analytics/indicator/activity_base.php
mod/feedback/classes/analytics/indicator/activity_base.php
mod/feedback/classes/analytics/indicator/cognitive_depth.php
mod/feedback/classes/analytics/indicator/social_breadth.php
mod/feedback/lib.php
mod/feedback/tests/external_test.php
mod/feedback/tests/lib_test.php
mod/lesson/classes/analytics/indicator/activity_base.php
mod/lesson/tests/external_test.php
mod/quiz/classes/analytics/indicator/activity_base.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/report/responses/tests/responses_from_steps_walkthrough_test.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
mod/quiz/tests/attempt_walkthrough_from_csv_test.php
mod/quiz/tests/calendar_event_modified_test.php [new file with mode: 0644]
mod/quiz/tests/locallib_test.php
mod/scorm/classes/analytics/indicator/activity_base.php
mod/workshop/classes/analytics/indicator/activity_base.php
mod/workshop/tests/external_test.php
notes/lib.php
notes/tests/behat/participants_notes.feature
phpunit.xml.dist
report/insights/classes/output/insights_list.php
report/insights/lang/en/report_insights.php
report/insights/templates/insights_list.mustache
report/outline/lang/en/report_outline.php
report/outline/tests/behat/outline.feature
report/participation/index.php
report/participation/module.js [deleted file]
report/participation/tests/behat/message_participants.feature
search/tests/fixtures/mock_search_engine.php
search/tests/manager_test.php
theme/boost/scss/moodle/calendar.scss
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_calendar/event_summary_body.mustache
user/action_redir.php
user/addnote.php [deleted file]
user/amd/build/participants.min.js [new file with mode: 0644]
user/amd/src/participants.js [new file with mode: 0644]
user/groupaddnote.php [deleted file]
user/index.php
user/module.js [deleted file]
user/renderer.php
user/templates/add_bulk_note.mustache [new file with mode: 0644]
user/templates/send_bulk_message.mustache [new file with mode: 0644]
user/tests/behat/behat_user.php [new file with mode: 0644]
user/tests/behat/enrol_cohort_list.feature [new file with mode: 0644]
user/tests/externallib_test.php
version.php

index 6542585..5139a7c 100644 (file)
@@ -13,7 +13,7 @@ language: php
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
-    - 7.1
+    - 7.2
     - 7.0
 
 addons:
@@ -30,7 +30,7 @@ env:
     # Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
     # start first so that the total run time is not too high.
     #
-    # We only run MySQL on PHP 7.1, so run that first.
+    # We only run MySQL on PHP 7.2, so run that first.
     # CI Tests should be second-highest in priority as these only take <= 60 seconds to run under normal circumstances.
     # Postgres is significantly is pretty reasonable in its run-time.
 
@@ -54,12 +54,12 @@ matrix:
 
     include:
           # Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
-        - php: 7.1
+        - php: 7.2
           env: DB=none     TASK=GRUNT   NVM_VERSION='8.9'
 
     exclude:
         # MySQL - it's just too slow.
-        # Exclude it on all versions except for 7.1
+        # Exclude it on all versions except for 7.2
 
         - env: DB=mysqli   TASK=PHPUNIT
           php: 7.0
index 404b010..4e5ea9b 100644 (file)
@@ -156,11 +156,12 @@ function tool_analytics_calculate_course_dates($course, $options) {
         $format = course_get_format($course);
         $formatoptions = $format->get_format_options();
 
-        if ($course->format === 'weeks' && $formatoptions['automaticenddate']) {
-            // Special treatment for weeks with automatic end date.
+        // Change this for a course formats API level call in MDL-60702.
+        if (method_exists($format, 'update_end_date') && $formatoptions['automaticenddate']) {
+            // Special treatment for weeks-based formats with automatic end date.
 
             if ($options['update']) {
-                format_weeks::update_end_date($course->id);
+                $format::update_end_date($course->id);
                 $course->enddate = $DB->get_field('course', 'enddate', array('id' => $course->id));
                 $notification .= PHP_EOL . '  ' . get_string('weeksenddateautomaticallyset', 'tool_analytics') . ': ' .
                     userdate($course->enddate);
index a4da374..6bda848 100644 (file)
@@ -74,7 +74,7 @@ if ($options['run']) {
         }
     }
     $_SERVER['argv'] = array_values($_SERVER['argv']);
-    PHPUnit_TextUI_Command::main();
+    PHPUnit\TextUI\Command::main();
     exit(0);
 }
 
index fed39db..b723abf 100644 (file)
@@ -547,172 +547,6 @@ class course implements \core_analytics\analysable {
         return $grades;
     }
 
-    /**
-     * Guesses all activities that were available during a period of time.
-     *
-     * @param string $activitytype
-     * @param int $starttime
-     * @param int $endtime
-     * @param \stdClass $student
-     * @return array
-     */
-    public function get_activities($activitytype, $starttime, $endtime, $student = false) {
-
-        // Var $student may not be available, default to not calculating dynamic data.
-        $studentid = -1;
-        if ($student) {
-            $studentid = $student->id;
-        }
-        $modinfo = get_fast_modinfo($this->get_course_data(), $studentid);
-        $activities = $modinfo->get_instances_of($activitytype);
-
-        $timerangeactivities = array();
-        foreach ($activities as $activity) {
-            if (!$this->completed_by($activity, $starttime, $endtime)) {
-                continue;
-            }
-
-            $timerangeactivities[$activity->context->id] = $activity;
-        }
-
-        return $timerangeactivities;
-    }
-
-    /**
-     * Was the activity supposed to be completed during the provided time range?.
-     *
-     * @param \cm_info $activity
-     * @param int $starttime
-     * @param int $endtime
-     * @return bool
-     */
-    protected function completed_by(\cm_info $activity, $starttime, $endtime) {
-
-        // We can't check uservisible because:
-        // - Any activity with available until would not be counted.
-        // - Sites may block student's course view capabilities once the course is closed.
-
-        // Students can not view hidden activities by default, this is not reliable 100% but accurate in most of the cases.
-        if ($activity->visible === false) {
-            return false;
-        }
-
-        // We skip activities that were not yet visible or their 'until' was not in this $starttime - $endtime range.
-        if ($activity->availability) {
-            $info = new \core_availability\info_module($activity);
-            $activityavailability = $this->availability_completed_by($info, $starttime, $endtime);
-            if ($activityavailability === false) {
-                return false;
-            } else if ($activityavailability === true) {
-                // This activity belongs to this time range.
-                return true;
-            }
-        }
-
-        // We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
-        $section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
-        if ($section->availability) {
-            $info = new \core_availability\info_section($section);
-            $sectionavailability = $this->availability_completed_by($info, $starttime, $endtime);
-            if ($sectionavailability === false) {
-                return false;
-            } else if ($sectionavailability === true) {
-                // This activity belongs to this section time range.
-                return true;
-            }
-        }
-
-        // When the course is using format weeks we use the week's end date.
-        $format = course_get_format($activity->get_modinfo()->get_course());
-        if ($this->get_course_data()->format === 'weeks') {
-            $dates = $format->get_section_dates($section);
-
-            // We need to consider the +2 hours added by get_section_dates.
-            // Avoid $starttime <= $dates->end because $starttime may be the start of the next week.
-            if ($starttime < ($dates->end - 7200) && $endtime >= ($dates->end - 7200)) {
-                return true;
-            } else {
-                return false;
-            }
-        }
-
-        if ($activity->sectionnum == 0) {
-            return false;
-        }
-
-        if (!$this->get_end() || !$this->get_start()) {
-            debugging('Activities which due date is in a time range can not be calculated ' .
-                'if the course doesn\'t have start and end date', DEBUG_DEVELOPER);
-            return false;
-        }
-
-        if (!course_format_uses_sections($this->get_course_data()->format)) {
-            // If it does not use sections and there are no availability conditions to access it it is available
-            // and we can not magically classify it into any other time range than this one.
-            return true;
-        }
-
-        // Split the course duration in the number of sections and consider the end of each section the due
-        // date of all activities contained in that section.
-        $formatoptions = $format->get_format_options();
-        if (!empty($formatoptions['numsections'])) {
-            $nsections = $formatoptions['numsections'];
-        } else {
-            // There are course format that use sections but without numsections, we fallback to the number
-            // of cached sections in get_section_info_all, not that accurate though.
-            $coursesections = $activity->get_modinfo()->get_section_info_all();
-            $nsections = count($coursesections);
-            if (isset($coursesections[0])) {
-                // We don't count section 0 if it exists.
-                $nsections--;
-            }
-        }
-
-        $courseduration = $this->get_end() - $this->get_start();
-        $sectionduration = round($courseduration / $nsections);
-        $activitysectionenddate = $this->get_start() + ($sectionduration * $activity->sectionnum);
-        if ($activitysectionenddate > $starttime && $activitysectionenddate <= $endtime) {
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Check if the activity/section should have been completed during the provided period according to its availability rules.
-     *
-     * @param \core_availability\info $info
-     * @param int $starttime
-     * @param int $endtime
-     * @return bool|null
-     */
-    protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
-
-        $dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
-        foreach ($dateconditions as $condition) {
-            // Availability API does not allow us to check from / to dates nicely, we need to be naughty.
-            $conditiondata = $condition->save();
-
-            if ($conditiondata->d === \availability_date\condition::DIRECTION_FROM &&
-                    $conditiondata->t > $endtime) {
-                // Skip this activity if any 'from' date is later than the end time.
-                return false;
-
-            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
-                    ($conditiondata->t < $starttime || $conditiondata->t > $endtime)) {
-                // Skip activity if any 'until' date is not in $starttime - $endtime range.
-                return false;
-            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
-                    $conditiondata->t < $endtime && $conditiondata->t > $starttime) {
-                return true;
-            }
-        }
-
-        // This can be interpreted as 'the activity was available but we don't know if its expected completion date
-        // was during this period.
-        return null;
-    }
-
     /**
      * Used by get_user_ids to extract the user id.
      *
index 1e7da1d..5deaeb6 100644 (file)
@@ -35,6 +35,13 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class community_of_inquiry_activity extends linear {
 
+    /**
+     * instancedata
+     *
+     * @var array
+     */
+    protected $instancedata = array();
+
     /**
      * @var \core_analytics\course
      */
@@ -478,8 +485,8 @@ abstract class community_of_inquiry_activity extends linear {
             // Samples are at cm level or below.
             $useractivities = array(\context_module::instance($cm->id)->id => $cm);
         } else {
-            // All course activities.
-            $useractivities = $this->course->get_activities($this->get_activity_type(), $starttime, $endtime, $user);
+            // Activities that should be completed during this time period.
+            $useractivities = $this->get_activities($starttime, $endtime, $user);
         }
 
         return $useractivities;
@@ -741,6 +748,250 @@ abstract class community_of_inquiry_activity extends linear {
         $this->grades = $course->get_student_grades($courseactivities);
     }
 
+    /**
+     * Guesses all activities that were available during a period of time.
+     *
+     * @param int $starttime
+     * @param int $endtime
+     * @param \stdClass|false $student
+     * @return array
+     */
+    protected function get_activities($starttime, $endtime, $student = false) {
+
+        $activitytype = $this->get_activity_type();
+
+        // Var $student may not be available, default to not calculating dynamic data.
+        $studentid = -1;
+        if ($student) {
+            $studentid = $student->id;
+        }
+        $modinfo = get_fast_modinfo($this->course->get_course_data(), $studentid);
+        $activities = $modinfo->get_instances_of($activitytype);
+
+        $timerangeactivities = array();
+        foreach ($activities as $activity) {
+
+            if (!$this->activity_completed_by($activity, $starttime, $endtime, $student)) {
+                continue;
+            }
+
+            $timerangeactivities[$activity->context->id] = $activity;
+        }
+
+        return $timerangeactivities;
+    }
+
+    /**
+     * Was the activity supposed to be completed during the provided time range?.
+     *
+     * @param \cm_info $activity
+     * @param int $starttime
+     * @param int $endtime
+     * @param \stdClass|false $student
+     * @return bool
+     */
+    protected function activity_completed_by(\cm_info $activity, $starttime, $endtime, $student = false) {
+
+        // We can't check uservisible because:
+        // - Any activity with available until would not be counted.
+        // - Sites may block student's course view capabilities once the course is closed.
+
+        // Students can not view hidden activities by default, this is not reliable 100% but accurate in most of the cases.
+        if ($activity->visible === false) {
+            return false;
+        }
+
+        // Give priority to the different methods activities have to set a "due" date.
+        $return = $this->activity_type_completed_by($activity, $starttime, $endtime, $student);
+        if (!is_null($return)) {
+            // Method activity_type_completed_by returns null if there is no due date method or there is but it is not set.
+            return $return;
+        }
+
+        // We skip activities that were not yet visible or their 'until' was not in this $starttime - $endtime range.
+        if ($activity->availability) {
+            $info = new \core_availability\info_module($activity);
+            $activityavailability = $this->availability_completed_by($info, $starttime, $endtime);
+            if ($activityavailability === false) {
+                return false;
+            } else if ($activityavailability === true) {
+                // This activity belongs to this time range.
+                return true;
+            }
+        }
+
+        // We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
+        $section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
+        if ($section->availability) {
+            $info = new \core_availability\info_section($section);
+            $sectionavailability = $this->availability_completed_by($info, $starttime, $endtime);
+            if ($sectionavailability === false) {
+                return false;
+            } else if ($sectionavailability === true) {
+                // This activity belongs to this section time range.
+                return true;
+            }
+        }
+
+        // When the course is using format weeks we use the week's end date.
+        $format = course_get_format($activity->get_modinfo()->get_course());
+        // We should change this in MDL-60702.
+        if (method_exists($format, 'get_section_dates')) {
+            $dates = $format->get_section_dates($section);
+
+            // We need to consider the +2 hours added by get_section_dates.
+            // Avoid $starttime <= $dates->end because $starttime may be the start of the next week.
+            if ($starttime < ($dates->end - 7200) && $endtime >= ($dates->end - 7200)) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        if ($activity->sectionnum == 0) {
+            return false;
+        }
+
+        if (!$this->course->get_end() || !$this->course->get_start()) {
+            debugging('Activities which due date is in a time range can not be calculated ' .
+                'if the course doesn\'t have start and end date', DEBUG_DEVELOPER);
+            return false;
+        }
+
+        if (!course_format_uses_sections($this->course->get_course_data()->format)) {
+            // If it does not use sections and there are no availability conditions to access it it is available
+            // and we can not magically classify it into any other time range than this one.
+            return true;
+        }
+
+        // Split the course duration in the number of sections and consider the end of each section the due
+        // date of all activities contained in that section.
+        $formatoptions = $format->get_format_options();
+        if (!empty($formatoptions['numsections'])) {
+            $nsections = $formatoptions['numsections'];
+        } else {
+            // There are course format that use sections but without numsections, we fallback to the number
+            // of cached sections in get_section_info_all, not that accurate though.
+            $coursesections = $activity->get_modinfo()->get_section_info_all();
+            $nsections = count($coursesections);
+            if (isset($coursesections[0])) {
+                // We don't count section 0 if it exists.
+                $nsections--;
+            }
+        }
+
+        $courseduration = $this->course->get_end() - $this->course->get_start();
+        $sectionduration = round($courseduration / $nsections);
+        $activitysectionenddate = $this->course->get_start() + ($sectionduration * $activity->sectionnum);
+        if ($activitysectionenddate > $starttime && $activitysectionenddate <= $endtime) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * True if the activity is due or it has been closed during this period, false if during another period, null if no due time.
+     *
+     * It can be overwritten by activities that allow teachers to set a due date or a time close separately
+     * from Moodle availability system. Note that in most of the cases overwriting get_timeclose_field should
+     * be enough.
+     *
+     * Returns true or false if the time close date falls into the provided time range. Null otherwise.
+     *
+     * @param \cm_info $activity
+     * @param int $starttime
+     * @param int $endtime
+     * @param \stdClass|false $student
+     * @return null
+     */
+    protected function activity_type_completed_by(\cm_info $activity, $starttime, $endtime, $student = false) {
+
+        $fieldname = $this->get_timeclose_field();
+        if (!$fieldname) {
+            // This activity type do not have its own availability control.
+            return null;
+        }
+
+        $this->fill_instance_data($activity);
+        $instance = $this->instancedata[$activity->instance];
+
+        if (!$instance->{$fieldname}) {
+            return null;
+        }
+
+        if ($starttime < $instance->{$fieldname} && $endtime >= $instance->{$fieldname}) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the name of the field that controls activity availability.
+     *
+     * Should be overwritten by activities that allow teachers to set a due date or a time close separately
+     * from Moodle availability system.
+     *
+     * Just 1 field will not be enough for all cases, but for the most simple ones without
+     * overrides and stuff like that.
+     *
+     * @return null|string
+     */
+    protected function get_timeclose_field() {
+        return null;
+    }
+
+    /**
+     * Check if the activity/section should have been completed during the provided period according to its availability rules.
+     *
+     * @param \core_availability\info $info
+     * @param int $starttime
+     * @param int $endtime
+     * @return bool|null
+     */
+    protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
+
+        $dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
+        foreach ($dateconditions as $condition) {
+            // Availability API does not allow us to check from / to dates nicely, we need to be naughty.
+            $conditiondata = $condition->save();
+
+            if ($conditiondata->d === \availability_date\condition::DIRECTION_FROM &&
+                    $conditiondata->t > $endtime) {
+                // Skip this activity if any 'from' date is later than the end time.
+                return false;
+
+            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
+                    ($conditiondata->t < $starttime || $conditiondata->t > $endtime)) {
+                // Skip activity if any 'until' date is not in $starttime - $endtime range.
+                return false;
+            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
+                    $conditiondata->t < $endtime && $conditiondata->t > $starttime) {
+                return true;
+            }
+        }
+
+        // This can be interpreted as 'the activity was available but we don't know if its expected completion date
+        // was during this period.
+        return null;
+    }
+
+    /**
+     * Fills in activity instance data.
+     *
+     * @param \cm_info $cm
+     * @return void
+     */
+    protected function fill_instance_data(\cm_info $cm) {
+        global $DB;
+
+        if (!isset($this->instancedata[$cm->instance])) {
+            $this->instancedata[$cm->instance] = $DB->get_record($this->get_activity_type(), array('id' => $cm->instance),
+                '*', MUST_EXIST);
+        }
+    }
+
     /**
      * Defines indicator type.
      *
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Unit tests for course activities.
+ * Unit tests for activities completed by classification.
  *
  * @package   core_analytics
  * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Unit tests for course activities
+ * Unit tests for activities completed by classification.
  *
  * @package   core_analytics
  * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_analytics_course_activities_testcase extends advanced_testcase {
+class community_of_inquiry_activities_completed_by_testcase extends advanced_testcase {
 
     /**
      * availability_levels
@@ -59,8 +59,6 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
         // Forum1 is ignored as section 0 does not count.
         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
 
-        $courseman = new \core_analytics\course($course);
-
         $modinfo = get_fast_modinfo($course, $stu1->id);
         $cm = $modinfo->get_cm($forum->cmid);
 
@@ -93,20 +91,23 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
 
         $cm = $modinfo->get_cm($forum->cmid);
 
+        $course = new \core_analytics\course($course);
+        list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
+
         // Condition from after provided end time.
-        $this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-20 00:00:00 GMT'),
+        $this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-20 00:00:00 GMT'),
             strtotime('2015-10-21 00:00:00 GMT'), $stu1));
 
         // Condition until before provided start time
-        $this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-25 00:00:00 GMT'),
+        $this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-25 00:00:00 GMT'),
             strtotime('2015-10-26 00:00:00 GMT'), $stu1));
 
         // Condition until after provided end time.
-        $this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'),
+        $this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
             strtotime('2015-10-23 00:00:00 GMT'), $stu1));
 
         // Condition until after provided start time and before provided end time.
-        $this->assertCount(1, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'),
+        $this->assertCount(1, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
             strtotime('2015-10-25 00:00:00 GMT'), $stu1));
     }
 
@@ -138,7 +139,8 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
         $forum5 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
             array('section' => 4));
 
-        $courseman = new \core_analytics\course($course);
+        $course = new \core_analytics\course($course);
+        list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
 
         $this->setUser($stu1);
 
@@ -146,10 +148,10 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
         $second = $startdate + WEEKSECS;
         $third = $startdate + (WEEKSECS * 2);
         $forth = $startdate + (WEEKSECS * 3);
-        $this->assertCount(1, $courseman->get_activities('forum', $first, $first + WEEKSECS, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $second, $second + WEEKSECS, $stu1));
-        $this->assertCount(0, $courseman->get_activities('forum', $third, $third + WEEKSECS, $stu1));
-        $this->assertCount(2, $courseman->get_activities('forum', $forth, $forth + WEEKSECS, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $second, $second + WEEKSECS, $stu1));
+        $this->assertCount(0, $method->invoke($indicator, $third, $third + WEEKSECS, $stu1));
+        $this->assertCount(2, $method->invoke($indicator, $forth, $forth + WEEKSECS, $stu1));
     }
 
     /**
@@ -189,7 +191,8 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
         $forum6 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
             array('section' => 12));
 
-        $courseman = new \core_analytics\course($course);
+        $course = new \core_analytics\course($course);
+        list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
 
         $this->setUser($stu1);
 
@@ -199,10 +202,10 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
         $second = $startdate + $duration;
         $third = $startdate + ($duration * 2);
         $forth = $startdate + ($duration * 3);
-        $this->assertCount(1, $courseman->get_activities('forum', $first, $first + $duration, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $second, $second + $duration, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $third, $third + $duration, $stu1));
-        $this->assertCount(2, $courseman->get_activities('forum', $forth, $forth + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $first, $first + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $second, $second + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $third, $third + $duration, $stu1));
+        $this->assertCount(2, $method->invoke($indicator, $forth, $forth + $duration, $stu1));
 
         // Split the course in as many parts as sections.
         $duration = ($enddate - $startdate) / $numsections;
@@ -210,15 +213,94 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
             // The -1 because section 1 start represents the course start.
             $timeranges[$i] = $startdate + ($duration * ($i - 1));
         }
-        $this->assertCount(1, $courseman->get_activities('forum', $timeranges[1], $timeranges[1] + $duration, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $timeranges[4], $timeranges[4] + $duration, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $timeranges[8], $timeranges[8] + $duration, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $timeranges[10], $timeranges[10] + $duration, $stu1));
-        $this->assertCount(1, $courseman->get_activities('forum', $timeranges[12], $timeranges[12] + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $timeranges[1], $timeranges[1] + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $timeranges[4], $timeranges[4] + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $timeranges[8], $timeranges[8] + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $timeranges[10], $timeranges[10] + $duration, $stu1));
+        $this->assertCount(1, $method->invoke($indicator, $timeranges[12], $timeranges[12] + $duration, $stu1));
 
         // Nothing here.
-        $this->assertCount(0, $courseman->get_activities('forum', $timeranges[2], $timeranges[2] + $duration, $stu1));
-        $this->assertCount(0, $courseman->get_activities('forum', $timeranges[3], $timeranges[3] + $duration, $stu1));
+        $this->assertCount(0, $method->invoke($indicator, $timeranges[2], $timeranges[2] + $duration, $stu1));
+        $this->assertCount(0, $method->invoke($indicator, $timeranges[3], $timeranges[3] + $duration, $stu1));
+    }
+
+    /**
+     * test_get_activities_with_specific_restrictions
+     *
+     * @return void
+     */
+    public function test_get_activities_with_specific_restrictions() {
+
+        list($course, $stu1) = $this->setup_course();
+
+        $end = strtotime('2015-10-24 00:00:00 GMT');
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $assign1 = $this->getDataGenerator()->create_module('assign', $params);
+        $params['duedate'] = $end;
+        $assign2 = $this->getDataGenerator()->create_module('assign', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $choice1 = $this->getDataGenerator()->create_module('choice', $params);
+        $params['timeclose'] = $end;
+        $choice1 = $this->getDataGenerator()->create_module('choice', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $data1 = $this->getDataGenerator()->create_module('data', $params);
+        $params['timeavailableto'] = $end;
+        $data1 = $this->getDataGenerator()->create_module('data', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $feedback1 = $this->getDataGenerator()->create_module('feedback', $params);
+        $params['timeclose'] = $end;
+        $feedback1 = $this->getDataGenerator()->create_module('feedback', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $lesson1 = $this->getDataGenerator()->create_module('lesson', $params);
+        $params['deadline'] = $end;
+        $lesson1 = $this->getDataGenerator()->create_module('lesson', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $quiz1 = $this->getDataGenerator()->create_module('quiz', $params);
+        $params['timeclose'] = $end;
+        $quiz1 = $this->getDataGenerator()->create_module('quiz', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $scorm1 = $this->getDataGenerator()->create_module('scorm', $params);
+        $params['timeclose'] = $end;
+        $scorm1 = $this->getDataGenerator()->create_module('scorm', $params);
+
+        // 1 with time close, one without.
+        $params = array('course' => $course->id);
+        $workshop1 = $this->getDataGenerator()->create_module('workshop', $params);
+        $params['submissionend'] = $end;
+        $workshop1 = $this->getDataGenerator()->create_module('workshop', $params);
+
+        $course = new \core_analytics\course($course);
+
+        $activitytypes = array('mod_assign', 'mod_choice', 'mod_data', 'mod_feedback', 'mod_lesson',
+            'mod_quiz', 'mod_scorm', 'mod_workshop');
+        foreach ($activitytypes as $activitytype) {
+
+            list($indicator, $method) = $this->instantiate_indicator($activitytype, $course);
+
+            $message = $activitytype . ' activity type returned activities do not match expected size';
+            $this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-20 00:00:00 GMT'),
+                strtotime('2015-10-21 00:00:00 GMT'), $stu1), $message);
+
+            $this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-25 00:00:00 GMT'),
+                strtotime('2015-10-26 00:00:00 GMT'), $stu1), $message);
+
+            $this->assertCount(1, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
+                strtotime('2015-10-25 00:00:00 GMT'), $stu1), $message);
+        }
     }
 
     /**
@@ -243,4 +325,27 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
         return array($course, $stu1);
     }
 
+    /**
+     * Returns the module cognitive depth indicator and the reflection method.
+     *
+     * @param string $modulename
+     * @param \core_analytics\course $course
+     * @return array
+     */
+    private function instantiate_indicator($modulename, \core_analytics\course $course) {
+
+        $classname = '\\' . $modulename . '\analytics\indicator\cognitive_depth';
+        $indicator = new $classname();
+        $class = new ReflectionClass($indicator);
+
+        $property = $class->getProperty('course');
+        $property->setAccessible(true);
+        $property->setValue($indicator, $course);
+
+        $method = new ReflectionMethod($indicator, 'get_activities');
+        $method->setAccessible(true);
+
+        return array($indicator, $method);
+    }
+
 }
index 7a693b5..fdb9541 100644 (file)
@@ -121,7 +121,7 @@ $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? --
 $string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n";
 $string['ldapnotconfigured'] = 'The LDAP host url is currently not configured';
 $string['morethanoneuser'] = 'Strange! More than one user record found in ldap. Only using the first one.';
-$string['needbcmath'] = 'You need the BCMath extension to use grace logins with Active Directory';
+$string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.';
 $string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory';
 $string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}';
 $string['noemail'] = 'Tried to send you an email but failed!';
index 2183370..505ff27 100644 (file)
@@ -37,7 +37,11 @@ class block_html extends block_base {
     }
 
     function specialization() {
-        $this->title = isset($this->config->title) ? format_string($this->config->title) : format_string(get_string('newhtmlblock', 'block_html'));
+        if (isset($this->config->title)) {
+            $this->title = $this->title = format_string($this->config->title, true, ['context' => $this->context]);
+        } else {
+            $this->title = get_string('newhtmlblock', 'block_html');
+        }
     }
 
     function instance_allow_multiple() {
index 7313a59..7a451fb 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 4c29f20..299aece 100644 (file)
@@ -339,28 +339,37 @@ define([
         };
 
         /**
-         * Get the CSS class to apply for the given event type.
+         * Convert the given event type into one of either user, site,
+         * group, category, or course.
          *
          * @param {String} eventType The calendar event type
          * @return {String}
          */
-        var getEventTypeClassFromType = function(eventType) {
+        var normaliseEventType = function(eventType) {
             switch (eventType) {
                 case 'user':
-                    return 'calendar_event_user';
+                    return 'user';
                 case 'site':
-                    return 'calendar_event_site';
+                    return 'site';
                 case 'group':
-                    return 'calendar_event_group';
+                    return 'group';
                 case 'category':
-                    return 'calendar_event_category';
-                case 'course':
-                    return 'calendar_event_course';
+                    return 'category';
                 default:
-                    return 'calendar_event_course';
+                    return 'course';
             }
         };
 
+        /**
+         * Get the CSS class to apply for the given event type.
+         *
+         * @param {String} eventType The calendar event type
+         * @return {String}
+         */
+        var getEventTypeClassFromType = function(eventType) {
+            return 'calendar_event_' + normaliseEventType(eventType);
+        };
+
         /**
          * Render the event summary modal.
          *
@@ -419,7 +428,7 @@ define([
          * @return {promise} The lang string promise.
          */
         var getEventType = function(eventType) {
-            var lang = 'type' + eventType;
+            var lang = 'type' + normaliseEventType(eventType);
             return Str.get_string(lang, 'core_calendar').then(function(langStr) {
                 return langStr;
             });
index f7d2555..ad97c96 100644 (file)
@@ -74,6 +74,10 @@ class calendar_event_exporter extends event_exporter_base {
             'type' => PARAM_TEXT,
             'optional' => true
         ];
+        $values['draggable'] = [
+            'type' => PARAM_BOOL,
+            'default' => false
+        ];
 
         return $values;
     }
@@ -90,6 +94,10 @@ class calendar_event_exporter extends event_exporter_base {
         $values = parent::get_other_values($output);
         $event = $this->event;
 
+        // By default all events that can be edited are
+        // draggable.
+        $values['draggable'] = $values['canedit'];
+
         if ($moduleproxy = $event->get_course_module()) {
             $modulename = $moduleproxy->get('modname');
             $moduleid = $moduleproxy->get('id');
@@ -213,17 +221,30 @@ class calendar_event_exporter extends event_exporter_base {
      * @return array
      */
     protected function get_module_timestamp_limits($event) {
+        global $DB;
+
         $values = [];
         $mapper = container::get_event_mapper();
         $starttime = $event->get_times()->get_start_time();
+        $modname = $event->get_course_module()->get('modname');
+        $modid = $event->get_course_module()->get('instance');
+        $moduleinstance = $DB->get_record($modname, ['id' => $modid]);
 
         list($min, $max) = component_callback(
-            'mod_' . $event->get_course_module()->get('modname'),
+            'mod_' . $modname,
             'core_calendar_get_valid_event_timestart_range',
-            [$mapper->from_event_to_legacy_event($event)],
-            [null, null]
+            [$mapper->from_event_to_legacy_event($event), $moduleinstance],
+            [false, false]
         );
 
+        // The callback will return false for either of the
+        // min or max cutoffs to indicate that there are no
+        // valid timestart values. In which case the event is
+        // not draggable.
+        if ($min === false || $max === false) {
+            return ['draggable' => false];
+        }
+
         if ($min) {
             $values = array_merge($values, $this->get_module_timestamp_min_limit($starttime, $min));
         }
index 00713f4..6da620e 100644 (file)
@@ -231,9 +231,12 @@ class api {
         event_interface $event,
         \DateTimeInterface $startdate
     ) {
+        global $DB;
+
         $mapper = container::get_event_mapper();
         $legacyevent = $mapper->from_event_to_legacy_event($event);
         $hascoursemodule = !empty($event->get_course_module());
+        $moduleinstance = null;
         $starttime = $event->get_times()->get_start_time()->setDate(
             $startdate->format('Y'),
             $startdate->format('n'),
@@ -241,15 +244,38 @@ class api {
         );
 
         if ($hascoursemodule) {
+            $moduleinstance = $DB->get_record(
+                $event->get_course_module()->get('modname'),
+                ['id' => $event->get_course_module()->get('instance')],
+                '*',
+                MUST_EXIST
+            );
             $legacyevent->timestart = $starttime->getTimestamp();
-            // If this event is from an activity then we need to call
-            // the activity callback to let it validate that the changes
-            // to the event are correct.
-            component_callback(
+
+            // If there is a timestart range callback implemented then we can
+            // use the values returned from the valid timestart range to apply
+            // some default validation on the event's timestart value to ensure
+            // that it falls within the specified range.
+            list($min, $max) = component_callback(
                 'mod_' . $event->get_course_module()->get('modname'),
-                'core_calendar_validate_event_timestart',
-                [$legacyevent]
+                'core_calendar_get_valid_event_timestart_range',
+                [$legacyevent, $moduleinstance],
+                [false, false]
             );
+
+            // If the callback returns false for either value it means that
+            // there is no valid time start range.
+            if ($min === false || $max === false) {
+                throw new \moodle_exception('The start day of this event can not be modified');
+            }
+
+            if ($min && $legacyevent->timestart < $min[0]) {
+                throw new \moodle_exception($min[1]);
+            }
+
+            if ($max && $legacyevent->timestart > $max[0]) {
+                throw new \moodle_exception($max[1]);
+            }
         }
 
         // This function does our capability checks.
@@ -262,7 +288,7 @@ class api {
         // We don't want to call the event update callback if the user isn't allowed
         // to modify course modules because depending on the callback it can make
         // some changes that would be considered security issues, such as updating the
-        // due date for and assignment.
+        // due date for an assignment.
         if ($hascoursemodule && calendar_edit_event_allowed($legacyevent, true)) {
             // If this event is from an activity then we need to call
             // the activity callback to let it know that the event it
@@ -270,7 +296,7 @@ class api {
             component_callback(
                 'mod_' . $event->get_course_module()->get('modname'),
                 'core_calendar_event_timestart_updated',
-                [$legacyevent]
+                [$legacyevent, $moduleinstance]
             );
         }
 
index 0c7a824..1e36407 100644 (file)
@@ -37,6 +37,8 @@ require_once($CFG->dirroot.'/lib/formslib.php');
  */
 class create extends \moodleform {
 
+    use eventtype;
+
     /**
      * Build the editor options using the given context.
      *
@@ -183,102 +185,6 @@ class create extends \moodleform {
         $mform->setDefault('visible', 1);
     }
 
-    /**
-     * Add the appropriate elements for the available event types.
-     *
-     * If the only event type available is 'user' then we add a hidden
-     * element because there is nothing for the user to choose.
-     *
-     * If more than one type is available then we add the elements as
-     * follows:
-     *      - Always add the event type selector
-     *      - Elements per type:
-     *          - course: add an additional select element with each
-     *                    course as an option.
-     *          - group: add a select element for the course (different
-     *                   from the above course select) and a select
-     *                   element for the group.
-     *
-     * @param MoodleQuickForm $mform
-     * @param array $eventtypes The available event types for the user
-     */
-    protected function add_event_type_elements($mform, $eventtypes) {
-        $options = [];
-
-        if (isset($eventtypes['user'])) {
-            $options['user'] = get_string('user');
-        }
-        if (isset($eventtypes['group'])) {
-            $options['group'] = get_string('group');
-        }
-        if (isset($eventtypes['course'])) {
-            $options['course'] = get_string('course');
-        }
-        if (isset($eventtypes['category'])) {
-            $options['category'] = get_string('category');
-        }
-        if (isset($eventtypes['site'])) {
-            $options['site'] = get_string('site');
-        }
-
-        // If we only have one event type and it's 'user' event then don't bother
-        // rendering the select boxes because there is no choice for the user to
-        // make.
-        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
-            $mform->addElement('hidden', 'eventtype');
-            $mform->setType('eventtype', PARAM_TEXT);
-            $mform->setDefault('eventtype', 'user');
-
-            // Render a static element to tell the user what type of event will
-            // be created.
-            $mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
-            return;
-        } else {
-            $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
-        }
-
-        if (isset($eventtypes['category'])) {
-            $categoryoptions = [];
-            foreach ($eventtypes['category'] as $id => $category) {
-                $categoryoptions[$id] = $category;
-            }
-
-            $mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
-            $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
-        }
-
-        if (isset($eventtypes['course'])) {
-            $limit = !has_capability('moodle/calendar:manageentries', context_system::instance());
-            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
-            $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
-        }
-
-        if (isset($eventtypes['group'])) {
-            $options = ['limittoenrolled' => true];
-            // Exclude courses without group.
-            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
-                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
-                    array_keys($eventtypes['groupcourses']));
-            }
-
-            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
-            $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
-
-            $groupoptions = [];
-            foreach ($eventtypes['group'] as $group) {
-                // We are formatting it this way in order to provide the javascript both
-                // the course and group ids so that it can enhance the form for the user.
-                $index = "{$group->courseid}-{$group->id}";
-                $groupoptions[$index] = format_string($group->name, true,
-                    ['context' => \context_course::instance($group->courseid)]);
-            }
-
-            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
-            $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
-            // We handle the group select hide/show actions on the event_form module.
-        }
-    }
-
     /**
      * Add the various elements to express the duration options available
      * for an event.
diff --git a/calendar/classes/local/event/forms/eventtype.php b/calendar/classes/local/event/forms/eventtype.php
new file mode 100644 (file)
index 0000000..59a9cd7
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The trait for adding eventtype fields to a form.
+ *
+ * @package     core_calendar
+ * @copyright   2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\local\event\forms;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The trait for adding eventtype fields to a form.
+ *
+ * @copyright   2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait eventtype {
+
+    /**
+     * Add the appropriate elements for the available event types.
+     *
+     * If the only event type available is 'user' then we add a hidden
+     * element because there is nothing for the user to choose.
+     *
+     * If more than one type is available then we add the elements as
+     * follows:
+     *      - Always add the event type selector
+     *      - Elements per type:
+     *          - course: add an additional select element with each
+     *                    course as an option.
+     *          - group: add a select element for the course (different
+     *                   from the above course select) and a select
+     *                   element for the group.
+     *
+     * @param MoodleQuickForm $mform
+     * @param array $eventtypes The available event types for the user
+     */
+    protected function add_event_type_elements($mform, $eventtypes) {
+        $options = [];
+
+        if (isset($eventtypes['user'])) {
+            $options['user'] = get_string('user');
+        }
+        if (isset($eventtypes['group'])) {
+            $options['group'] = get_string('group');
+        }
+        if (isset($eventtypes['course'])) {
+            $options['course'] = get_string('course');
+        }
+        if (isset($eventtypes['category'])) {
+            $options['category'] = get_string('category');
+        }
+        if (isset($eventtypes['site'])) {
+            $options['site'] = get_string('site');
+        }
+
+        // If we only have one event type and it's 'user' event then don't bother
+        // rendering the select boxes because there is no choice for the user to
+        // make.
+        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
+            $mform->addElement('hidden', 'eventtype');
+            $mform->setType('eventtype', PARAM_TEXT);
+            $mform->setDefault('eventtype', 'user');
+
+            // Render a static element to tell the user what type of event will
+            // be created.
+            $mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
+            return;
+        } else {
+            $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
+        }
+
+        if (isset($eventtypes['category'])) {
+            $categoryoptions = [];
+            foreach ($eventtypes['category'] as $id => $category) {
+                $categoryoptions[$id] = $category;
+            }
+
+            $mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
+            $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
+        }
+
+        if (isset($eventtypes['course'])) {
+            $limit = !has_capability('moodle/calendar:manageentries', \context_system::instance());
+            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
+            $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
+        }
+
+        if (isset($eventtypes['group'])) {
+            $options = ['limittoenrolled' => true];
+            // Exclude courses without group.
+            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
+                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
+                    array_keys($eventtypes['groupcourses']));
+            }
+
+            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
+            $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
+
+            $groupoptions = [];
+            foreach ($eventtypes['group'] as $group) {
+                // We are formatting it this way in order to provide the javascript both
+                // the course and group ids so that it can enhance the form for the user.
+                $index = "{$group->courseid}-{$group->id}";
+                $groupoptions[$index] = format_string($group->name, true,
+                    ['context' => \context_course::instance($group->courseid)]);
+            }
+
+            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
+            $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
+            // We handle the group select hide/show actions on the event_form module.
+        }
+    }
+}
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @package calendar
  */
+namespace core_calendar\local\event\forms;
 
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+defined('MOODLE_INTERNAL') || die();
 
-require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir . '/formslib.php');
 
 /**
  * Form for adding a subscription to a Moodle course calendar.
  * @copyright 2012 Jonathan Harker
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class calendar_addsubscription_form extends moodleform {
+class managesubscriptions extends \moodleform {
+
+    use eventtype;
 
     /**
      * Defines the form used to add calendar subscriptions.
      */
     public function definition() {
         $mform = $this->_form;
-        $courseid = optional_param('course', 0, PARAM_INT);
+        $eventtypes = calendar_get_all_allowed_types();
+        if (empty($eventtypes)) {
+            print_error('nopermissiontoupdatecalendar');
+        }
 
         $mform->addElement('header', 'addsubscriptionform', get_string('importcalendarheading', 'calendar'));
 
@@ -77,24 +81,10 @@ class calendar_addsubscription_form extends moodleform {
         $mform->disabledIf('url',  'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
         $mform->disabledIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL);
 
-        // Eventtype: 0 = user, 1 = global, anything else = course ID.
-        list($choices, $groups) = calendar_get_eventtype_choices($courseid);
-        $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $choices);
-        $mform->addRule('eventtype', get_string('required'), 'required');
-        $mform->setType('eventtype', PARAM_ALPHA);
-
-        if (!empty($groups) and is_array($groups)) {
-            $groupoptions = array();
-            foreach ($groups as $group) {
-                $groupoptions[$group->id] = $group->name;
-            }
-            $mform->addElement('select', 'groupid', get_string('typegroup', 'calendar'), $groupoptions);
-            $mform->setType('groupid', PARAM_INT);
-            $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
-        }
+        // Add the select elements for the available event types.
+        $this->add_event_type_elements($mform, $eventtypes);
 
-        $mform->addElement('hidden', 'course');
-        $mform->setType('course', PARAM_INT);
+        // Eventtype: 0 = user, 1 = global, anything else = course ID.
         $mform->addElement('submit', 'add', get_string('add'));
     }
 
@@ -110,6 +100,14 @@ class calendar_addsubscription_form extends moodleform {
 
         $errors = parent::validation($data, $files);
 
+        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
+        $eventtypes = calendar_get_all_allowed_types();
+        $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+
+        if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
+            $errors['eventtype'] = get_string('invalideventtype', 'calendar');
+        }
+
         if ($data['importfrom'] == CALENDAR_IMPORT_FROM_FILE) {
             if (empty($data['importfile'])) {
                 $errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar');
@@ -117,7 +115,7 @@ class calendar_addsubscription_form extends moodleform {
                 // Make sure the file area is not empty and contains only one file.
                 $draftitemid = $data['importfile'];
                 $fs = get_file_storage();
-                $usercontext = context_user::instance($USER->id);
+                $usercontext = \context_user::instance($USER->id);
                 $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id DESC', false);
                 if (count($files) !== 1) {
                     $errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar');
@@ -140,7 +138,7 @@ class calendar_addsubscription_form extends moodleform {
     public function definition_after_data() {
         $mform =& $this->_form;
 
-        $mform->applyFilter('url', 'calendar_addsubscription_form::strip_webcal');
+        $mform->applyFilter('url', static::class . '::strip_webcal');
         $mform->applyFilter('url', 'trim');
     }
 
index ce344ee..0ccbf31 100644 (file)
@@ -203,6 +203,8 @@ class core_calendar_external extends external_api {
             $courses = $params['events']['courseids'];
             $funcparam['courses'] = $courses;
         }
+        // Now get categories we can get events from.
+        $categories = \coursecat::get_all();
 
         // Let us findout groups that we can return events from.
         if (!$hassystemcap) {
@@ -273,7 +275,8 @@ class core_calendar_external extends external_api {
             } else {
                 // Can the user actually see this event?
                 $eventobj = calendar_event::load($eventobj);
-                if (($eventobj->courseid == $SITE->id) ||
+                if ((($eventobj->courseid == $SITE->id) && (empty($eventobj->categoryid))) ||
+                            (!empty($eventobj->categoryid) && in_array($eventobj->categoryid, $categories)) ||
                             (!empty($eventobj->groupid) && in_array($eventobj->groupid, $groups)) ||
                             (!empty($eventobj->courseid) && in_array($eventobj->courseid, $courses)) ||
                             ($USER->id == $eventobj->userid) ||
@@ -723,6 +726,14 @@ class core_calendar_external extends external_api {
         $warnings = array();
 
         $legacyevent = calendar_event::load($eventid);
+        // Must check we can see this event.
+        if (!calendar_view_event_allowed($legacyevent)) {
+            // We can't return a warning in this case because the event is not optional.
+            // We don't know the context for the event and it's not worth loading it.
+            $syscontext = context_system::instance();
+            throw new \required_capability_exception($syscontext, 'moodle/course:view', 'nopermission', '');
+        }
+
         $legacyevent->count_repeats();
 
         $eventmapper = event_container::get_event_mapper();
@@ -976,7 +987,7 @@ class core_calendar_external extends external_api {
         $calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']);
         self::validate_context($calendar->context);
 
-        list($data, $template) = calendar_get_view($calendar, 'day', $params['includenavigation']);
+        list($data, $template) = calendar_get_view($calendar, 'day');
 
         return $data;
     }
index 78386e2..8e76d89 100644 (file)
@@ -2113,6 +2113,91 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
     return array($courses, $group, $user);
 }
 
+/**
+ * Return the capability for viewing a calendar event.
+ *
+ * @param calendar_event $event event object
+ * @return boolean
+ */
+function calendar_view_event_allowed(calendar_event $event) {
+    global $USER;
+
+    // Anyone can see site events.
+    if ($event->courseid && $event->courseid == SITEID) {
+        return true;
+    }
+
+    // If a user can manage events at the site level they can see any event.
+    $sitecontext = \context_system::instance();
+    // If user has manageentries at site level, return true.
+    if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
+        return true;
+    }
+
+    if (!empty($event->groupid)) {
+        // If it is a group event we need to be able to manage events in the course, or be in the group.
+        if (has_capability('moodle/calendar:manageentries', $event->context) ||
+                has_capability('moodle/calendar:managegroupentries', $event->context)) {
+            return true;
+        }
+
+        $mycourses = enrol_get_my_courses('id');
+        return isset($mycourses[$event->courseid]) && groups_is_member($event->groupid);
+    } else if ($event->modulename) {
+        // If this is a module event we need to be able to see the module.
+        $coursemodules = [];
+        $courseid = 0;
+        // Override events do not have the courseid set.
+        if ($event->courseid) {
+            $courseid = $event->courseid;
+            $coursemodules = get_fast_modinfo($event->courseid)->instances;
+        } else {
+            $cmraw = get_coursemodule_from_instance($event->modulename, $event->instance, 0, false, MUST_EXIST);
+            $courseid = $cmraw->course;
+            $coursemodules = get_fast_modinfo($cmraw->course)->instances;
+        }
+        $hasmodule = isset($coursemodules[$event->modulename]);
+        $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
+
+        // If modinfo doesn't know about the module, return false to be safe.
+        if (!$hasmodule || !$hasinstance) {
+            return false;
+        }
+
+        // Must be able to see the course and the module - MDL-59304.
+        $cm = $coursemodules[$event->modulename][$event->instance];
+        if (!$cm->uservisible) {
+            return false;
+        }
+        $mycourses = enrol_get_my_courses('id');
+        return isset($mycourses[$courseid]);
+    } else if ($event->categoryid) {
+        // If this is a category we need to be able to see the category.
+        $cat = \coursecat::get($event->categoryid, IGNORE_MISSING);
+        if (!$cat) {
+            return false;
+        }
+        return true;
+    } else if (!empty($event->courseid)) {
+        // If it is a course event we need to be able to manage events in the course, or be in the course.
+        if (has_capability('moodle/calendar:manageentries', $event->context)) {
+            return true;
+        }
+        $mycourses = enrol_get_my_courses('id');
+        return isset($mycourses[$event->courseid]);
+    } else if ($event->userid) {
+        if ($event->userid != $USER->id) {
+            // No-one can ever see another users events.
+            return false;
+        }
+        return true;
+    } else {
+        throw new moodle_exception('unknown event type');
+    }
+
+    return false;
+}
+
 /**
  * Return the capability for editing calendar event.
  *
@@ -2664,12 +2749,36 @@ function calendar_get_eventtype_choices($courseid) {
 function calendar_add_subscription($sub) {
     global $DB, $USER, $SITE;
 
+    // Undo the form definition work around to allow us to have two different
+    // course selectors present depending on which event type the user selects.
+    if (!empty($sub->groupcourseid)) {
+        $sub->courseid = $sub->groupcourseid;
+        unset($sub->groupcourseid);
+    }
+
+    // Pull the group id back out of the value. The form saves the value
+    // as "<courseid>-<groupid>" to allow the javascript to work correctly.
+    if (!empty($sub->groupid)) {
+        list($courseid, $groupid) = explode('-', $sub->groupid);
+        $sub->courseid = $courseid;
+        $sub->groupid = $groupid;
+    }
+
+    // Default course id if none is set.
+    if (empty($sub->courseid)) {
+        if ($sub->eventtype === 'site') {
+            $sub->courseid = SITEID;
+        } else {
+            $sub->courseid = 0;
+        }
+    }
+
     if ($sub->eventtype === 'site') {
         $sub->courseid = $SITE->id;
     } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
-        $sub->courseid = $sub->course;
+        $sub->courseid = $sub->courseid;
     } else if ($sub->eventtype === 'category') {
-        $sub->categoryid = $sub->category;
+        $sub->categoryid = $sub->categoryid;
     } else {
         // User events.
         $sub->courseid = 0;
@@ -2689,8 +2798,25 @@ function calendar_add_subscription($sub) {
             // Trigger event, calendar subscription added.
             $eventparams = array('objectid' => $sub->id,
                 'context' => calendar_get_calendar_context($sub),
-                'other' => array('eventtype' => $sub->eventtype, 'courseid' => $sub->courseid)
+                'other' => array(
+                    'eventtype' => $sub->eventtype,
+                )
             );
+            switch ($sub->eventtype) {
+                case 'category':
+                    $eventparams['other']['categoryid'] = $sub->categoryid;
+                    break;
+                case 'course':
+                    $eventparams['other']['courseid'] = $sub->courseid;
+                    break;
+                case 'group':
+                    $eventparams['other']['courseid'] = $sub->courseid;
+                    $eventparams['other']['groupid'] = $sub->groupid;
+                    break;
+                default:
+                    $eventparams['other']['courseid'] = $sub->courseid;
+            }
+
             $event = \core\event\calendar_subscription_created::create($eventparams);
             $event->trigger();
             return $id;
@@ -2708,13 +2834,13 @@ function calendar_add_subscription($sub) {
  * Add an iCalendar event to the Moodle calendar.
  *
  * @param stdClass $event The RFC-2445 iCalendar event
- * @param int $courseid The course ID
+ * @param int $unused Deprecated
  * @param int $subscriptionid The iCalendar subscription ID
  * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
  * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
  * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated,  CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
  */
-function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone='UTC') {
+function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $timezone='UTC') {
     global $DB;
 
     // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
@@ -2787,6 +2913,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
     $eventrecord->userid = $sub->userid;
     $eventrecord->groupid = $sub->groupid;
     $eventrecord->courseid = $sub->courseid;
+    $eventrecord->categoryid = $sub->categoryid;
     $eventrecord->eventtype = $sub->eventtype;
 
     if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid,
@@ -2867,8 +2994,24 @@ function calendar_delete_subscription($subscription) {
     // Trigger event, calendar subscription deleted.
     $eventparams = array('objectid' => $subscription->id,
         'context' => calendar_get_calendar_context($subscription),
-        'other' => array('courseid' => $subscription->courseid)
+        'other' => array(
+            'eventtype' => $subscription->eventtype,
+        )
     );
+    switch ($subscription->eventtype) {
+        case 'category':
+            $eventparams['other']['categoryid'] = $subscription->categoryid;
+            break;
+        case 'course':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            break;
+        case 'group':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            $eventparams['other']['groupid'] = $subscription->groupid;
+            break;
+        default:
+            $eventparams['other']['courseid'] = $subscription->courseid;
+    }
     $event = \core\event\calendar_subscription_deleted::create($eventparams);
     $event->trigger();
 }
@@ -2907,7 +3050,7 @@ function calendar_get_icalendar($url) {
  * @param int $subscriptionid The subscription ID.
  * @return string A log of the import progress, including errors.
  */
-function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = null) {
+function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) {
     global $DB;
 
     $return = '';
@@ -2934,7 +3077,7 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu
 
     $return = '';
     foreach ($ical->components['VEVENT'] as $event) {
-        $res = calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone);
+        $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone);
         switch ($res) {
             case CALENDAR_IMPORT_EVENT_UPDATED:
                 $updatecount++;
@@ -2984,7 +3127,7 @@ function calendar_update_subscription_events($subscriptionid) {
     }
 
     $ical = calendar_get_icalendar($sub->url);
-    $return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid);
+    $return = calendar_import_icalendar_events($ical, null, $subscriptionid);
     $sub->lastupdated = time();
 
     calendar_update_subscription($sub);
@@ -3019,8 +3162,24 @@ function calendar_update_subscription($subscription) {
     $eventparams = array('userid' => $subscription->userid,
         'objectid' => $subscription->id,
         'context' => calendar_get_calendar_context($subscription),
-        'other' => array('eventtype' => $subscription->eventtype, 'courseid' => $subscription->courseid)
+        'other' => array(
+            'eventtype' => $subscription->eventtype,
+        )
     );
+    switch ($subscription->eventtype) {
+        case 'category':
+            $eventparams['other']['categoryid'] = $subscription->categoryid;
+            break;
+        case 'course':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            break;
+        case 'group':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            $eventparams['other']['groupid'] = $subscription->groupid;
+            break;
+        default:
+            $eventparams['other']['courseid'] = $subscription->courseid;
+    }
     $event = \core\event\calendar_subscription_updated::create($eventparams);
     $event->trigger();
 }
@@ -3042,9 +3201,14 @@ function calendar_can_edit_subscription($subscriptionorid) {
 
     $allowed = new \stdClass;
     $courseid = $subscription->courseid;
+    $categoryid = $subscription->categoryid;
     $groupid = $subscription->groupid;
+    $category = null;
 
-    calendar_get_allowed_types($allowed, $courseid);
+    if (!empty($categoryid)) {
+        $category = \coursecat::get($categoryid);
+    }
+    calendar_get_allowed_types($allowed, $courseid, null, $category);
     switch ($subscription->eventtype) {
         case 'user':
             return $allowed->user;
@@ -3054,6 +3218,12 @@ function calendar_can_edit_subscription($subscriptionorid) {
             } else {
                 return false;
             }
+        case 'category':
+            if (isset($allowed->categories[$categoryid])) {
+                return $allowed->categories[$categoryid];
+            } else {
+                return false;
+            }
         case 'site':
             return $allowed->site;
         case 'group':
index f1c74d6..75ddc09 100644 (file)
@@ -26,10 +26,10 @@ require_once('../config.php');
 require_once($CFG->libdir.'/bennu/bennu.inc.php');
 require_once($CFG->dirroot.'/course/lib.php');
 require_once($CFG->dirroot.'/calendar/lib.php');
-require_once($CFG->dirroot.'/calendar/managesubscriptions_form.php');
 
 // Required use.
-$courseid = optional_param('course', SITEID, PARAM_INT);
+$courseid = optional_param('course', null, PARAM_INT);
+$categoryid = optional_param('category', null, PARAM_INT);
 // Used for processing subscription actions.
 $subscriptionid = optional_param('id', 0, PARAM_INT);
 $pollinterval  = optional_param('pollinterval', 0, PARAM_INT);
@@ -39,6 +39,9 @@ $url = new moodle_url('/calendar/managesubscriptions.php');
 if ($courseid != SITEID) {
     $url->param('course', $courseid);
 }
+if ($categoryid) {
+    $url->param('categoryid', $categoryid);
+}
 navigation_node::override_active_url(new moodle_url('/calendar/view.php', array('view' => 'month')));
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('admin');
@@ -58,7 +61,7 @@ if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
 
-$form = new calendar_addsubscription_form(null);
+$form = new \core_calendar\local\event\forms\managesubscriptions();
 $form->set_data(array(
     'course' => $course->id
 ));
@@ -75,7 +78,7 @@ if (!empty($formdata)) {
         $calendar = $form->get_file_content('importfile');
         $ical = new iCalendar();
         $ical->unserialize($calendar);
-        $importresults = calendar_import_icalendar_events($ical, $courseid, $subscriptionid);
+        $importresults = calendar_import_icalendar_events($ical, null, $subscriptionid);
     } else {
         try {
             $importresults = calendar_update_subscription_events($subscriptionid);
@@ -102,11 +105,54 @@ if (!empty($formdata)) {
     }
 }
 
-$sql = 'SELECT *
-          FROM {event_subscriptions}
-         WHERE courseid = :courseid
-            OR (courseid = 0 AND userid = :userid)';
-$params = array('courseid' => $courseid, 'userid' => $USER->id);
+$types = calendar_get_all_allowed_types();
+
+$searches = [];
+$params = [];
+
+$usedefaultfilters = true;
+if (!empty($courseid) && $courseid == SITEID && isset($types['site'])) {
+    $searches[] = "(eventtype = 'site')";
+    $searches[] = "(eventtype = 'user' AND userid = :userid)";
+    $params['userid'] = $USER->id;
+    $usedefaultfilters = false;
+}
+
+if (!empty($courseid) && isset($types['course']) && array_key_exists($courseid, $types['course'])) {
+    $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid = :courseid)";
+    $params += ['courseid' => $courseid];
+    $usedefaultfilters = false;
+}
+
+if (!empty($categoryid) && isset($types['category']) && array_key_exists($categoryid, $types['category'])) {
+    $searches[] = "(eventtype = 'category' AND categoryid = :categoryid)";
+    $params += ['categoryid' => $categoryid];
+    $usedefaultfilters = false;
+}
+
+if ($usedefaultfilters) {
+    $searches[] = "(eventtype = 'user' AND userid = :userid)";
+    $params['userid'] = $USER->id;
+
+    if (isset($types['site'])) {
+        $searches[] = "(eventtype = 'site' AND courseid  = :siteid)";
+        $params += ['siteid' => SITEID];
+    }
+
+    if (isset($types['course'])) {
+        list($courseinsql, $courseparams) = $DB->get_in_or_equal(array_keys($types['course']), SQL_PARAMS_NAMED, 'course');
+        $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
+        $params += $courseparams;
+    }
+
+    if (isset($types['category'])) {
+        list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(array_keys($types['category']), SQL_PARAMS_NAMED, 'category');
+        $searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})";
+        $params += $categoryparams;
+    }
+}
+
+$sql = "SELECT * FROM {event_subscriptions} WHERE " . implode(' OR ', $searches);;
 $subscriptions = $DB->get_records_sql($sql, $params);
 
 // Print title and header.
index 7193a97..e8dadbe 100644 (file)
@@ -283,12 +283,12 @@ class core_calendar_renderer extends plugin_renderer_base {
     /**
      * Renders a table containing information about calendar subscriptions.
      *
-     * @param int $courseid
+     * @param int $unused
      * @param array $subscriptions
      * @param string $importresults
      * @return string
      */
-    public function subscription_details($courseid, $subscriptions, $importresults = '') {
+    public function subscription_details($unused = null, $subscriptions, $importresults = '') {
         $table = new html_table();
         $table->head  = array(
             get_string('colcalendar', 'calendar'),
@@ -318,7 +318,7 @@ class core_calendar_renderer extends plugin_renderer_base {
                 $lastupdated = userdate($sub->lastupdated, get_string('strftimedatetimeshort', 'langconfig'));
             }
 
-            $cell = new html_table_cell($this->subscription_action_form($sub, $courseid));
+            $cell = new html_table_cell($this->subscription_action_form($sub));
             $cell->colspan = 2;
             $type = $sub->eventtype . 'events';
 
@@ -342,10 +342,9 @@ class core_calendar_renderer extends plugin_renderer_base {
      * Creates a form to perform actions on a given subscription.
      *
      * @param stdClass $subscription
-     * @param int $courseid
      * @return string
      */
-    protected function subscription_action_form($subscription, $courseid) {
+    protected function subscription_action_form($subscription) {
         // Assemble form for the subscription row.
         $html = html_writer::start_tag('form', array('action' => new moodle_url('/calendar/managesubscriptions.php'), 'method' => 'post'));
         if (empty($subscription->url)) {
@@ -367,7 +366,6 @@ class core_calendar_renderer extends plugin_renderer_base {
             $html .= html_writer::end_tag('div');
         }
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
-        $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'course', 'value' => $courseid));
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id));
         $html .= html_writer::start_tag('div', array('class' => 'btn-group pull-right'));
         if (!empty($subscription->url)) {
index 0770cc5..de5424e 100644 (file)
@@ -29,6 +29,7 @@
     }
 }}
 <div{{!
+    }} class="summary-modal-container"{{!
     }} data-region="summary-modal-container"{{!
     }} data-event-id="{{id}}"{{!
     }} data-event-title="{{name}}"{{!
         {{#description}}
         <div class="row m-t-1">
             <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{.}}}</div>
+            <div class="description-content col-xs-11">{{{.}}}</div>
         </div>
         {{/description}}
+        {{#isactionevent}}
+        <div class="row m-t-1">
+            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+            <div class="col-xs-11"><a href="{{course.viewurl}}">{{{course.fullname}}}</a></div>
+        </div>
+        {{/isactionevent}}
         {{#iscategoryevent}}
         <div class="row m-t-1">
             <div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
         {{/groupname}}
         {{#subscription}}
             {{#displayeventsource}}
-            <div class="row m-t-1">
-                <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
-                <div class="col-xs-11">
-                    {{#url}}
-                        <a href="{{url}}">{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</a>
-                    {{/url}}
-                    {{^url}}
-                        <p>{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</p>
-                    {{/url}}
+                <div class="row m-t-1">
+                    <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
+                    <div class="col-xs-11">
+                        {{#subscriptionurl}}
+                            <p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
+                        {{/subscriptionurl}}
+                        {{^subscriptionurl}}
+                            <p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
+                        {{/subscriptionurl}}
+                    </div>
                 </div>
-            </div>
             {{/displayeventsource}}
         {{/subscription}}
     </div>
index 0cb0d8e..9abff8e 100644 (file)
@@ -87,7 +87,7 @@
                                         {{^underway}}
                                             <li data-region="event-item"
                                                 data-eventtype-{{calendareventtype}}="1"
-                                                {{#canedit}}
+                                                {{#draggable}}
                                                     draggable="true"
                                                     data-drag-type="move"
                                                     {{#mindaytimestamp}}
                                                     {{#maxdayerror}}
                                                         data-max-day-error="{{.}}"
                                                     {{/maxdayerror}}
-                                                {{/canedit}}>
+                                                {{/draggable}}>
 
                                                 <a data-action="view-event" data-event-id="{{id}}" href="{{url}}" title="{{name}}">
                                                     <span class="badge badge-circle calendar_event_{{calendareventtype}}">
index dea2495..e0e7850 100644 (file)
@@ -141,7 +141,7 @@ Feature: Perform basic calendar functionality
     When I click on "Test choice opens" "link"
     Then "Delete" "button" should not exist
     And "Edit" "button" should not exist
-    And I should see "Open event"
+    And I should see "Course event"
     When I click on "Go to activity" "link"
     And I wait to be redirected
     Then I should see "Test choice"
@@ -150,7 +150,7 @@ Feature: Perform basic calendar functionality
     When I click on "Test choice closes" "link"
     Then "Delete" "button" should not exist
     And "Edit" "button" should not exist
-    And I should see "Close event"
+    And I should see "Course event"
     When I click on "Go to activity" "link"
     And I wait to be redirected
     Then I should see "Test choice"
index c9a3829..6930b1e 100644 (file)
@@ -22,7 +22,7 @@ Feature: Import and edit calendar events
     And I set the following fields to these values:
       | Calendar name  | Test Import |
       | Import from    | Calendar file (.ics) |
-      | Type of event  | User events |
+      | Type of event  | User |
     And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager
     And I press "Add"
     And I should see "Events imported: 2"
index 6f78848..5750d50 100644 (file)
@@ -423,9 +423,9 @@ class core_calendar_events_testcase extends advanced_testcase {
     }
 
     /**
-     * Tests for calendar_subscription_added event.
+     * Tests for calendar_subscription_added event for a site subscription.
      */
-    public function test_calendar_subscription_created() {
+    public function test_calendar_subscription_created_site() {
         global $CFG;
         require_once($CFG->dirroot . '/calendar/lib.php');
         $this->resetAfterTest(true);
@@ -447,15 +447,115 @@ class core_calendar_events_testcase extends advanced_testcase {
         $this->assertEquals($id, $event->objectid);
         $this->assertEquals($subscription->courseid, $event->other['courseid']);
         $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
         $this->assertDebuggingNotCalled();
         $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_added event for a category subscription.
+     */
+    public function test_calendar_subscription_created_category() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $categoryid = $this->course->category;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'category';
+        $subscription->name = 'test';
+        $subscription->categoryid = $categoryid;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($categoryid, $event->other['categoryid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('courseid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_added event for a course subscription.
+     */
+    public function test_calendar_subscription_created_course() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'course';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($subscription->courseid, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Tests for calendar_subscription_added event for a group subscription.
+     */
+    public function test_calendar_subscription_created_group() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $courseid = $this->course->id;
+        $groupid = 42;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'group';
+        $subscription->name = 'test';
+        $subscription->groupid = "{$courseid}-{$groupid}";
 
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($courseid, $event->other['courseid']);
+        $this->assertEquals($groupid, $event->other['groupid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
     }
 
     /**
-     * Tests for calendar_subscription_updated event.
+     * Tests for calendar_subscription_updated event for a site subscription.
      */
-    public function test_calendar_subscription_updated() {
+    public function test_calendar_subscription_updated_site() {
         global $CFG;
         require_once($CFG->dirroot . '/calendar/lib.php');
         $this->resetAfterTest(true);
@@ -479,15 +579,120 @@ class core_calendar_events_testcase extends advanced_testcase {
         $this->assertEquals($subscription->id, $event->objectid);
         $this->assertEquals($subscription->courseid, $event->other['courseid']);
         $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
         $this->assertDebuggingNotCalled();
         $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_updated event for a category subscription.
+     */
+    public function test_calendar_subscription_updated_category() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $categoryid = $this->course->category;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'category';
+        $subscription->name = 'test';
+        $subscription->categoryid = $categoryid;
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
 
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($categoryid, $event->other['categoryid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('courseid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
     }
 
     /**
-     * Tests for calendar_subscription_deleted event.
+     * Tests for calendar_subscription_updated event for a group subscription.
      */
-    public function test_calendar_subscription_deleted() {
+    public function test_calendar_subscription_updated_course() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'course';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_updated event for a course subscription.
+     */
+    public function test_calendar_subscription_updated_group() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $courseid = $this->course->id;
+        $groupid = 42;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'group';
+        $subscription->name = 'test';
+        $subscription->groupid = "{$courseid}-{$groupid}";
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($groupid, $event->other['groupid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a site subscription.
+     */
+    public function test_calendar_subscription_deleted_site() {
         global $CFG;
         require_once($CFG->dirroot . '/calendar/lib.php');
         $this->resetAfterTest(true);
@@ -512,4 +717,102 @@ class core_calendar_events_testcase extends advanced_testcase {
         $sink->close();
 
     }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a category subscription.
+     */
+    public function test_calendar_subscription_deleted_category() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $categoryid = $this->course->category;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'category';
+        $subscription->name = 'test';
+        $subscription->categoryid = $categoryid;
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($categoryid, $event->other['categoryid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('courseid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a course.
+     */
+    public function test_calendar_subscription_deleted_course() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'course';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a group.
+     */
+    public function test_calendar_subscription_deleted_group() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $courseid = $this->course->id;
+        $groupid = 42;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'group';
+        $subscription->name = 'test';
+        $subscription->groupid = "{$courseid}-{$groupid}";
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($groupid, $event->other['groupid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
 }
index f60d57f..d1235c3 100644 (file)
@@ -2201,4 +2201,208 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertTrue($result['validationerror']);
     }
+
+    /**
+     * A user should not be able load the calendar monthly view for a course they cannot access.
+     */
+    public function test_get_calendar_monthly_view_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        $timestart = new DateTime();
+        // Admin can load the course.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_monthly_view_returns(),
+            core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
+                                                              $course->id, null, false)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User enrolled in the course can load the course calendar.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_monthly_view_returns(),
+            core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
+                                                              $course->id, null, false)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User not enrolled in the course cannot load the course calendar.
+        $this->setUser($user2);
+        $this->expectException('require_login_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_monthly_view_returns(),
+            core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
+                                                              $course->id, null, false)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar day view for a course they cannot access.
+     */
+    public function test_get_calendar_day_view_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        $timestart = new DateTime();
+        // Admin can load the course.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_day_view_returns(),
+            core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
+                                                          $timestart->format('j'), $course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User enrolled in the course can load the course calendar.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_day_view_returns(),
+            core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
+                                                          $timestart->format('j'), $course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User not enrolled in the course cannot load the course calendar.
+        $this->setUser($user2);
+        $this->expectException('require_login_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_day_view_returns(),
+            core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
+                                                          $timestart->format('j'), $course->id, null)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar upcoming view for a course they cannot access.
+     */
+    public function test_get_calendar_upcoming_view_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        // Admin can load the course.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_upcoming_view_returns(),
+            core_calendar_external::get_calendar_upcoming_view($course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User enrolled in the course can load the course calendar.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_upcoming_view_returns(),
+            core_calendar_external::get_calendar_upcoming_view($course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User not enrolled in the course cannot load the course calendar.
+        $this->setUser($user2);
+        $this->expectException('require_login_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_upcoming_view_returns(),
+            core_calendar_external::get_calendar_upcoming_view($course->id, null)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar event for a course they cannot access.
+     */
+    public function test_get_calendar_event_by_id_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        // Admin can load the course event.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_event_by_id_returns(),
+            core_calendar_external::get_calendar_event_by_id($courseevent->id)
+        );
+        $this->assertEquals($data['event']['id'], $courseevent->id);
+        // User enrolled in the course can load the course event.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_event_by_id_returns(),
+            core_calendar_external::get_calendar_event_by_id($courseevent->id)
+        );
+        $this->assertEquals($data['event']['id'], $courseevent->id);
+        // User not enrolled in the course cannot load the course event.
+        $this->setUser($user2);
+        $this->expectException('required_capability_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_event_by_id_returns(),
+            core_calendar_external::get_calendar_event_by_id($courseevent->id)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar events for a category they cannot see.
+     */
+    public function test_get_calendar_events_hidden_category() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $category = $generator->create_category(['visible' => 0]);
+        $name = 'Category Event (category: ' . $category->id . ')';
+        $record = new stdClass();
+        $record->categoryid = $category->id;
+        $categoryevent = $this->create_calendar_event($name, $USER->id, 'category', 0, time(), $record);
+
+        $events = [
+            'eventids' => [$categoryevent->id]
+        ];
+        $options = [];
+        // Admin can load the category event.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_events_returns(),
+            core_calendar_external::get_calendar_events($events, $options)
+        );
+        $this->assertEquals($data['events'][0]['id'], $categoryevent->id);
+        // User with no special permission to see hidden categories will not see the event.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_events_returns(),
+            core_calendar_external::get_calendar_events($events, $options)
+        );
+        $this->assertCount(0, $data['events']);
+        $this->assertEquals('nopermissions', $data['warnings'][0]['warningcode']);
+    }
 }
index ce2f1ef..081738e 100644 (file)
@@ -188,7 +188,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertEquals($ical->parser_errors, array());
 
         $sub = calendar_get_subscription($id);
-        calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        calendar_import_icalendar_events($ical, null, $sub->id);
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
 
@@ -205,7 +205,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertEquals($ical->parser_errors, array());
 
         $sub = calendar_get_subscription($id);
-        calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        calendar_import_icalendar_events($ical, null, $sub->id);
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
 
@@ -222,7 +222,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertEquals($ical->parser_errors, array());
 
         $sub = calendar_get_subscription($id);
-        calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        calendar_import_icalendar_events($ical, null, $sub->id);
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
     }
index 7f17a28..ff2e065 100644 (file)
@@ -44,6 +44,37 @@ class core_calendar_local_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
     }
 
+    /**
+     * Create a feedback activity instance and a calendar event for
+     * that instance.
+     *
+     * @param array $feedbackproperties Properties to set on the feedback activity
+     * @param array $eventproperties Properties to set on the calendar event
+     * @return array The feedback activity and the calendar event
+     */
+    protected function create_feedback_activity_and_event(array $feedbackproperties = [], array $eventproperties = []) {
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $mapper = container::get_event_mapper();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $feedback = $feedbackgenerator->create_instance(array_merge(
+            ['course' => $course->id],
+            $feedbackproperties
+        ));
+
+        $event = create_event(array_merge(
+            [
+                'courseid' => $course->id,
+                'modulename' => 'feedback',
+                'instance' => $feedback->id
+            ],
+             $eventproperties
+        ));
+        $event = $mapper->from_legacy_event_to_event($event);
+
+        return [$feedback, $event];
+    }
+
     /**
      * Requesting calendar events from a given time should return all events with a sort
      * time at or after the requested time. All events prior to that time should not
@@ -928,4 +959,330 @@ class core_calendar_local_api_testcase extends advanced_testcase {
         $this->expectException('moodle_exception');
         $newEvent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
     }
+
+    /**
+     * Updating the start day of an event with no maximum cutoff should
+     * update the corresponding activity property.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_no_max() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
+        $expected = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => 0
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+                'timestart' => $timeopen->getTimestamp()
+            ]
+        );
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+        $actual = $newevent->get_times()->get_start_time();
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+
+        $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
+        $this->assertEquals($expected->getTimestamp(), $feedback->timeopen);
+    }
+
+    /**
+     * Updating the start day of an event belonging to an activity to a value
+     * less than the maximum cutoff should update the corresponding activity
+     * property.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_less_than_max() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $timeclose = new DateTimeImmutable('2019-01-1T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
+        $expected = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => $timeclose->getTimestamp()
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+                'timestart' => $timeopen->getTimestamp()
+            ]
+        );
+
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+        $actual = $newevent->get_times()->get_start_time();
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+
+        $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
+        $this->assertEquals($expected->getTimestamp(), $feedback->timeopen);
+    }
+
+    /**
+     * Updating the start day of an event belonging to an activity to a value
+     * equal to the maximum cutoff should update the corresponding activity
+     * property.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_equal_to_max() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $timeclose = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => $timeclose->getTimestamp(),
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+                'timestart' => $timeopen->getTimestamp()
+            ]
+        );
+
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+        $actual = $newevent->get_times()->get_start_time();
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+
+        $this->assertEquals($timeclose->getTimestamp(), $actual->getTimestamp());
+        $this->assertEquals($timeclose->getTimestamp(), $feedback->timeopen);
+    }
+
+    /**
+     * Updating the start day of an event belonging to an activity to a value
+     * after the maximum cutoff should not update the corresponding activity
+     * property. Instead it should throw an exception.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_after_max() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $timeclose = new DateTimeImmutable('2017-02-2T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => $timeclose->getTimestamp(),
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+                'timestart' => $timeopen->getTimestamp()
+            ]
+        );
+
+        $this->expectException('moodle_exception');
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+    }
+
+    /**
+     * Updating the start day of an event with no minimum cutoff should
+     * update the corresponding activity property.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_no_min() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeclose = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2016-02-2T10:00:00+08:00');
+        $expected = new DateTimeImmutable('2016-02-2T15:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => 0,
+                'timeclose' => $timeclose->getTimestamp()
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+                'timestart' => $timeclose->getTimestamp()
+            ]
+        );
+
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+        $actual = $newevent->get_times()->get_start_time();
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+
+        $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
+        $this->assertEquals($expected->getTimestamp(), $feedback->timeopen);
+    }
+
+    /**
+     * Updating the start day of an event belonging to an activity to a value
+     * greater than the minimum cutoff should update the corresponding activity
+     * property.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_greater_than_min() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2016-01-1T15:00:00+08:00');
+        $timeclose = new DateTimeImmutable('2019-01-1T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
+        $expected = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => $timeclose->getTimestamp()
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+                'timestart' => $timeclose->getTimestamp()
+            ]
+        );
+
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+        $actual = $newevent->get_times()->get_start_time();
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+
+        $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
+        $this->assertEquals($expected->getTimestamp(), $feedback->timeclose);
+    }
+
+    /**
+     * Updating the start day of an event belonging to an activity to a value
+     * equal to the minimum cutoff should update the corresponding activity
+     * property.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_equal_to_min() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $timeclose = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2017-01-1T10:00:00+08:00');
+        $expected = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => $timeclose->getTimestamp(),
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+                'timestart' => $timeclose->getTimestamp()
+            ]
+        );
+
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+        $actual = $newevent->get_times()->get_start_time();
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+
+        $this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
+        $this->assertEquals($expected->getTimestamp(), $feedback->timeclose);
+    }
+
+    /**
+     * Updating the start day of an event belonging to an activity to a value
+     * before the minimum cutoff should not update the corresponding activity
+     * property. Instead it should throw an exception.
+     *
+     * Note: This test uses the feedback activity because it requires
+     * module callbacks to be in place to test.
+     */
+    public function test_update_event_start_day_activity_event_before_min() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/feedback/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $timeclose = new DateTimeImmutable('2017-02-2T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2016-02-2T10:00:00+08:00');
+        list($feedback, $event) = $this->create_feedback_activity_and_event(
+            [
+                'timeopen' => $timeopen->getTimestamp(),
+                'timeclose' => $timeclose->getTimestamp(),
+            ],
+            [
+                'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+                'timestart' => $timeclose->getTimestamp()
+            ]
+        );
+
+        $this->expectException('moodle_exception');
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+    }
+
+    /**
+     * Updating the start day of an overridden event belonging to an activity
+     * should result in an exception. This is to prevent the drag and drop
+     * of override events.
+     *
+     * Note: This test uses the quiz activity because it requires
+     * module callbacks to be in place and override event support to test.
+     */
+    public function test_update_event_start_day_activity_event_override() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        require_once($CFG->dirroot . '/mod/quiz/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $mapper = container::get_event_mapper();
+        $timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
+        $newstartdate = new DateTimeImmutable('2016-02-2T10:00:00+08:00');
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $quizgenerator = $generator->get_plugin_generator('mod_quiz');
+        $quiz = $quizgenerator->create_instance([
+            'course' => $course->id,
+            'timeopen' => $timeopen->getTimestamp(),
+        ]);
+        $event = create_event([
+            'courseid' => $course->id,
+            'userid' => $user->id,
+            'modulename' => 'quiz',
+            'instance' => $quiz->id,
+            'eventtype' => QUIZ_EVENT_TYPE_OPEN,
+            'timestart' => $timeopen->getTimestamp()
+        ]);
+        $event = $mapper->from_legacy_event_to_event($event);
+        $record = (object) [
+            'quiz' => $quiz->id,
+            'userid' => $user->id
+        ];
+
+        $DB->insert_record('quiz_overrides', $record);
+
+        $this->expectException('moodle_exception');
+        $newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
+    }
 }
index 0007c3c..528cc8d 100644 (file)
@@ -4,7 +4,7 @@ information provided here is intended especially for developers.
 === 3.4 ===
 * calendar_get_mini has been deprecated. Please update to use the new
   exporters and renderers.
-* added core_calendar_validate_event_timestart and core_calendar_event_timestart_updated callbacks for module events
+* added core_calendar_get_valid_event_timestart_range and core_calendar_event_timestart_updated callbacks for module events
   when the update_event_start_day function is used in the local api.
 
 === 3.3 ===
index 7595db0..7c38a8f 100644 (file)
@@ -324,7 +324,7 @@ class core_cohort_external extends external_api {
      * @return array
      */
     public static function search_cohorts($query, $context, $includes = 'parents', $limitfrom = 0, $limitnum = 25) {
-        global $DB, $CFG, $PAGE;
+        global $CFG;
         require_once($CFG->dirroot . '/cohort/lib.php');
 
         $params = self::validate_parameters(self::search_cohorts_parameters(), array(
@@ -341,7 +341,6 @@ class core_cohort_external extends external_api {
         $limitnum = $params['limitnum'];
 
         self::validate_context($context);
-        $output = $PAGE->get_renderer('tool_lp');
 
         $manager = has_capability('moodle/cohort:manage', $context);
         if (!$manager) {
index a656ec1..6203b8a 100644 (file)
@@ -5,8 +5,8 @@
     "type": "project",
     "homepage": "https://moodle.org",
     "require-dev": {
-        "phpunit/phpunit": "5.5.*",
-        "phpunit/dbUnit": "1.4.*",
+        "phpunit/phpunit": "6.4.*",
+        "phpunit/dbUnit": "3.0.*",
         "moodlehq/behat-extension": "3.34.1",
         "mikey179/vfsStream": "^1.6"
     }
index 1150efe..d69ba0b 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "1906bd3ac810927fb8084fe4e2967d36",
+    "content-hash": "b36746ace2486c033136c855a63f3793",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.6.1",
+            "version": "1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102"
+                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102",
-                "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.4.0"
+                "php": "^5.6 || ^7.0"
             },
             "require-dev": {
-                "doctrine/collections": "1.*",
-                "phpunit/phpunit": "~4.1"
+                "doctrine/collections": "^1.0",
+                "doctrine/common": "^2.6",
+                "phpunit/phpunit": "^4.1"
             },
             "type": "library",
             "autoload": {
                 "psr-4": {
                     "DeepCopy\\": "src/DeepCopy/"
-                }
+                },
+                "files": [
+                    "src/DeepCopy/deep_copy.php"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
             "description": "Create deep copies (clones) of your objects",
-            "homepage": "https://github.com/myclabs/DeepCopy",
             "keywords": [
                 "clone",
                 "copy",
                 "object",
                 "object graph"
             ],
-            "time": "2017-04-12T18:52:22+00:00"
+            "time": "2017-10-19T19:58:43+00:00"
+        },
+        {
+            "name": "phar-io/manifest",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/manifest.git",
+                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
+                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-phar": "*",
+                "phar-io/version": "^1.0.1",
+                "php": "^5.6 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+            "time": "2017-03-05T18:14:27+00:00"
+        },
+        {
+            "name": "phar-io/version",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/version.git",
+                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
+                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Library for handling version information and constraints",
+            "time": "2017-03-05T17:38:23+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
         },
         {
             "name": "phpunit/dbunit",
-            "version": "1.4.1",
+            "version": "3.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/dbunit.git",
-                "reference": "9aaee6447663ff1b0cd50c23637e04af74c5e2ae"
+                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/9aaee6447663ff1b0cd50c23637e04af74c5e2ae",
-                "reference": "9aaee6447663ff1b0cd50c23637e04af74c5e2ae",
+                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/6b9cec80dca8694243aade33bceb425ccafbbd0d",
+                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d",
                 "shasum": ""
             },
             "require": {
                 "ext-pdo": "*",
                 "ext-simplexml": "*",
-                "php": ">=5.3.3",
-                "phpunit/phpunit": "~4|~5",
-                "symfony/yaml": "~2.1|~3.0"
+                "php": "^7.0",
+                "phpunit/phpunit": "^6.0",
+                "symfony/yaml": "^3.0"
             },
-            "bin": [
-                "composer/bin/dbunit"
-            ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.3.x-dev"
+                    "dev-master": "3.0.x-dev"
                 }
             },
             "autoload": {
                 "classmap": [
-                    "PHPUnit/"
+                    "src/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                "",
-                "../../symfony/yaml/"
-            ],
             "license": [
                 "BSD-3-Clause"
             ],
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
-            "description": "DbUnit port for PHP/PHPUnit to support database interaction testing.",
+            "description": "PHPUnit extension for database interaction testing",
             "homepage": "https://github.com/sebastianbergmann/dbunit/",
             "keywords": [
                 "database",
                 "testing",
                 "xunit"
             ],
-            "time": "2015-08-07T04:57:38+00:00"
+            "time": "2017-10-19T13:21:48+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "4.0.8",
+            "version": "5.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d"
+                "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
-                "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8ed1902a57849e117b5651fc1a5c48110946c06b",
+                "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-xmlwriter": "*",
-                "php": "^5.6 || ^7.0",
-                "phpunit/php-file-iterator": "^1.3",
-                "phpunit/php-text-template": "^1.2",
-                "phpunit/php-token-stream": "^1.4.2 || ^2.0",
-                "sebastian/code-unit-reverse-lookup": "^1.0",
-                "sebastian/environment": "^1.3.2 || ^2.0",
-                "sebastian/version": "^1.0 || ^2.0"
+                "php": "^7.0",
+                "phpunit/php-file-iterator": "^1.4.2",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-token-stream": "^1.4.11 || ^2.0",
+                "sebastian/code-unit-reverse-lookup": "^1.0.1",
+                "sebastian/environment": "^3.0",
+                "sebastian/version": "^2.0.1",
+                "theseer/tokenizer": "^1.1"
             },
             "require-dev": {
-                "ext-xdebug": "^2.1.4",
-                "phpunit/phpunit": "^5.7"
+                "ext-xdebug": "^2.5",
+                "phpunit/phpunit": "^6.0"
             },
             "suggest": {
-                "ext-xdebug": "^2.5.1"
+                "ext-xdebug": "^2.5.5"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.0.x-dev"
+                    "dev-master": "5.2.x-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-04-02T07:44:40+00:00"
+            "time": "2017-08-03T12:40:43+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "5.5.7",
+            "version": "6.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "3f67cee782c9abfaee5e32fd2f57cdd54bc257ba"
+                "reference": "06b28548fd2b4a20c3cd6e247dc86331a7d4db13"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3f67cee782c9abfaee5e32fd2f57cdd54bc257ba",
-                "reference": "3f67cee782c9abfaee5e32fd2f57cdd54bc257ba",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/06b28548fd2b4a20c3cd6e247dc86331a7d4db13",
+                "reference": "06b28548fd2b4a20c3cd6e247dc86331a7d4db13",
                 "shasum": ""
             },
             "require": {
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
                 "ext-xml": "*",
-                "myclabs/deep-copy": "~1.3",
-                "php": "^5.6 || ^7.0",
-                "phpspec/prophecy": "^1.3.1",
-                "phpunit/php-code-coverage": "^4.0.1",
-                "phpunit/php-file-iterator": "~1.4",
-                "phpunit/php-text-template": "~1.2",
-                "phpunit/php-timer": "^1.0.6",
-                "phpunit/phpunit-mock-objects": "^3.2",
-                "sebastian/comparator": "~1.1",
-                "sebastian/diff": "~1.2",
-                "sebastian/environment": "^1.3 || ^2.0",
-                "sebastian/exporter": "~1.2",
-                "sebastian/global-state": "~1.0",
-                "sebastian/object-enumerator": "~1.0",
-                "sebastian/resource-operations": "~1.0",
-                "sebastian/version": "~1.0|~2.0",
-                "symfony/yaml": "~2.1|~3.0"
+                "myclabs/deep-copy": "^1.6.1",
+                "phar-io/manifest": "^1.0.1",
+                "phar-io/version": "^1.0",
+                "php": "^7.0",
+                "phpspec/prophecy": "^1.7",
+                "phpunit/php-code-coverage": "^5.2.2",
+                "phpunit/php-file-iterator": "^1.4.2",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-timer": "^1.0.9",
+                "phpunit/phpunit-mock-objects": "^4.0.3",
+                "sebastian/comparator": "^2.0.2",
+                "sebastian/diff": "^2.0",
+                "sebastian/environment": "^3.1",
+                "sebastian/exporter": "^3.1",
+                "sebastian/global-state": "^2.0",
+                "sebastian/object-enumerator": "^3.0.3",
+                "sebastian/resource-operations": "^1.0",
+                "sebastian/version": "^2.0.1"
             },
             "conflict": {
-                "phpdocumentor/reflection-docblock": "3.0.2"
+                "phpdocumentor/reflection-docblock": "3.0.2",
+                "phpunit/dbunit": "<3.0"
             },
             "require-dev": {
                 "ext-pdo": "*"
             },
             "suggest": {
-                "ext-tidy": "*",
                 "ext-xdebug": "*",
-                "phpunit/php-invoker": "~1.1"
+                "phpunit/php-invoker": "^1.1"
             },
             "bin": [
                 "phpunit"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.5.x-dev"
+                    "dev-master": "6.4.x-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2016-10-03T13:04:15+00:00"
+            "time": "2017-10-16T13:18:59+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "3.4.4",
+            "version": "4.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "a23b761686d50a560cc56233b9ecf49597cc9118"
+                "reference": "2f789b59ab89669015ad984afa350c4ec577ade0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118",
-                "reference": "a23b761686d50a560cc56233b9ecf49597cc9118",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/2f789b59ab89669015ad984afa350c4ec577ade0",
+                "reference": "2f789b59ab89669015ad984afa350c4ec577ade0",
                 "shasum": ""
             },
             "require": {
-                "doctrine/instantiator": "^1.0.2",
-                "php": "^5.6 || ^7.0",
-                "phpunit/php-text-template": "^1.2",
-                "sebastian/exporter": "^1.2 || ^2.0"
+                "doctrine/instantiator": "^1.0.5",
+                "php": "^7.0",
+                "phpunit/php-text-template": "^1.2.1",
+                "sebastian/exporter": "^3.0"
             },
             "conflict": {
-                "phpunit/phpunit": "<5.4.0"
+                "phpunit/phpunit": "<6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.4"
+                "phpunit/phpunit": "^6.0"
             },
             "suggest": {
                 "ext-soap": "*"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.2.x-dev"
+                    "dev-master": "4.0.x-dev"
                 }
             },
             "autoload": {
                 "mock",
                 "xunit"
             ],
-            "time": "2017-06-30T09:13:00+00:00"
+            "time": "2017-08-03T14:08:16+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "sebastian/comparator",
-            "version": "1.2.4",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
+                "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
-                "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/ae068fede81d06e7bb9bb46a367210a3d3e1fe6a",
+                "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3",
-                "sebastian/diff": "~1.2",
-                "sebastian/exporter": "~1.2 || ~2.0"
+                "php": "^7.0",
+                "sebastian/diff": "^2.0",
+                "sebastian/exporter": "^3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.4"
+                "phpunit/phpunit": "^6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2.x-dev"
+                    "dev-master": "2.0.x-dev"
                 }
             },
             "autoload": {
                 "compare",
                 "equality"
             ],
-            "time": "2017-01-29T09:50:25+00:00"
+            "time": "2017-08-03T07:14:59+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "1.4.3",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4"
+                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4",
-                "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
+                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0"
+                "php": "^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+                "phpunit/phpunit": "^6.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "diff"
             ],
-            "time": "2017-05-22T07:24:03+00:00"
+            "time": "2017-08-03T08:09:46+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "2.0.0",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac"
+                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
-                "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
+                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": "^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.0"
+                "phpunit/phpunit": "^6.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "3.1.x-dev"
                 }
             },
             "autoload": {
                 "environment",
                 "hhvm"
             ],
-            "time": "2016-11-26T07:53:53+00:00"
+            "time": "2017-07-01T08:51:00+00:00"
         },
         {
             "name": "sebastian/exporter",
-            "version": "1.2.2",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4"
+                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4",
-                "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
+                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3",
-                "sebastian/recursion-context": "~1.0"
+                "php": "^7.0",
+                "sebastian/recursion-context": "^3.0"
             },
             "require-dev": {
                 "ext-mbstring": "*",
-                "phpunit/phpunit": "~4.4"
+                "phpunit/phpunit": "^6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.3.x-dev"
+                    "dev-master": "3.1.x-dev"
                 }
             },
             "autoload": {
                 "export",
                 "exporter"
             ],
-            "time": "2016-06-17T09:04:28+00:00"
+            "time": "2017-04-03T13:19:02+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "1.1.1",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
+                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
-                "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.2"
+                "phpunit/phpunit": "^6.0"
             },
             "suggest": {
                 "ext-uopz": "*"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "global state"
             ],
-            "time": "2015-10-12T03:26:01+00:00"
+            "time": "2017-04-27T15:39:26+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
-            "version": "1.0.0",
+            "version": "3.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/object-enumerator.git",
-                "reference": "d4ca2fb70344987502567bc50081c03e6192fb26"
+                "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26",
-                "reference": "d4ca2fb70344987502567bc50081c03e6192fb26",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+                "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.6",
-                "sebastian/recursion-context": "~1.0"
+                "php": "^7.0",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~5"
+                "phpunit/phpunit": "^6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "3.0.x-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Traverses array structures and object graphs to enumerate all referenced objects",
             "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
-            "time": "2016-01-28T13:25:10+00:00"
+            "time": "2017-08-03T12:35:26+00:00"
+        },
+        {
+            "name": "sebastian/object-reflector",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-reflector.git",
+                "reference": "773f97c67f28de00d397be301821b06708fca0be"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
+                "reference": "773f97c67f28de00d397be301821b06708fca0be",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Allows reflection of object attributes, including inherited and non-public ones",
+            "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+            "time": "2017-03-29T09:07:27+00:00"
         },
         {
             "name": "sebastian/recursion-context",
-            "version": "1.0.5",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7"
+                "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7",
-                "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+                "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.4"
+                "phpunit/phpunit": "^6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "3.0.x-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "time": "2016-10-03T07:41:43+00:00"
+            "time": "2017-03-03T06:23:57+00:00"
         },
         {
             "name": "sebastian/resource-operations",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "aee7120b058c268363e606ff5fe8271da849a1b5"
+                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/aee7120b058c268363e606ff5fe8271da849a1b5",
-                "reference": "aee7120b058c268363e606ff5fe8271da849a1b5",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/317d5bdf0127f06db7ea294186132b4f5b036839",
+                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "9c69968ce57924e9e93550895cd2b0477edf0e19"
+                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/9c69968ce57924e9e93550895cd2b0477edf0e19",
-                "reference": "9c69968ce57924e9e93550895cd2b0477edf0e19",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/7572c904b209fa9907c69a6a9a68243c265a4d01",
+                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "f9f19a39ee178f61bb2190f51ff7c517c2159315"
+                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/f9f19a39ee178f61bb2190f51ff7c517c2159315",
-                "reference": "f9f19a39ee178f61bb2190f51ff7c517c2159315",
+                "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
+                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2017-09-04T16:28:07+00:00"
+            "time": "2017-10-04T18:56:58+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "a1e1b01293a090cb9ae2ddd221a3251a4a7e4abf"
+                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/a1e1b01293a090cb9ae2ddd221a3251a4a7e4abf",
-                "reference": "a1e1b01293a090cb9ae2ddd221a3251a4a7e4abf",
+                "url": "https://api.github.com/repos/symfony/console/zipball/116bc56e45a8e5572e51eb43ab58c769a352366c",
+                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-09-06T16:40:18+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "c5f5263ed231f164c58368efbce959137c7d9488"
+                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/c5f5263ed231f164c58368efbce959137c7d9488",
-                "reference": "c5f5263ed231f164c58368efbce959137c7d9488",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/07447650225ca9223bd5c97180fe7c8267f7d332",
+                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "8beb24eec70b345c313640962df933499373a944"
+                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/8beb24eec70b345c313640962df933499373a944",
-                "reference": "8beb24eec70b345c313640962df933499373a944",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
+                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2017-09-01T13:23:39+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "e593f06dd90a81c7b70ac1c49862a061b0ec06d2"
+                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e593f06dd90a81c7b70ac1c49862a061b0ec06d2",
-                "reference": "e593f06dd90a81c7b70ac1c49862a061b0ec06d2",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
+                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2017-09-05T20:39:38+00:00"
+            "time": "2017-10-04T17:15:30+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "6b511d7329b203a620f09a2288818d27dcc915ae"
+                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6b511d7329b203a620f09a2288818d27dcc915ae",
-                "reference": "6b511d7329b203a620f09a2288818d27dcc915ae",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/40dafd42d5dad7fe5ad4e958413d92a207522ac1",
+                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2017-09-11T15:55:22+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "54ca9520a00386f83bca145819ad3b619aaa2485"
+                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/54ca9520a00386f83bca145819ad3b619aaa2485",
-                "reference": "54ca9520a00386f83bca145819ad3b619aaa2485",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7ba037e4b8221956ab1e221c73c9e27e05dd423",
+                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb"
+                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/b32a0e5f928d0fa3d1dd03c78d020777e50c10cb",
-                "reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
+                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-03T13:33:10+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.5.0",
+            "version": "v1.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803"
+                "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803",
-                "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
+                "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.5-dev"
+                    "dev-master": "1.6-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2017-06-14T15:44:48+00:00"
+            "time": "2017-10-11T12:05:26+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.27",
+            "version": "v2.8.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8"
+                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8",
-                "reference": "57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8",
+                "url": "https://api.github.com/repos/symfony/process/zipball/26c9fb02bf06bd6b90f661a5bd17e510810d0176",
+                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-03T08:04:30+00:00"
+            "time": "2017-10-01T21:00:16+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "add53753d978f635492dfe8cd6953f6a7361ef90"
+                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/add53753d978f635492dfe8cd6953f6a7361ef90",
-                "reference": "add53753d978f635492dfe8cd6953f6a7361ef90",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/409bf229cd552bf7e3faa8ab7e3980b07672073f",
+                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-02T06:42:24+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.9",
+            "version": "v3.3.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0"
+                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/1d8c2a99c80862bdc3af94c1781bf70f86bccac0",
-                "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
+                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:54:42+00:00"
+            "time": "2017-10-05T14:43:42+00:00"
+        },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git",
+                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+            "time": "2017-04-07T12:08:54+00:00"
         },
         {
             "name": "webmozart/assert",
index f2598e0..87d0919 100644 (file)
@@ -69,10 +69,8 @@ class completion_enabled extends \core_analytics\local\indicator\binary {
 
         $course = $this->retrieve('course', $sampleid);
 
-        $cm = false;
-        if ($sampleorigin === 'course_modules') {
-            $cm = $this->retrieve('course_modules', $sampleid);
-        }
+        // It may not be available, but if it is the indicator checks if completion is enabled for the cm.
+        $cm = $this->retrieve('course_modules', $sampleid);
 
         $completion = new \completion_info($course);
 
index 8a0e6ec..427c130 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js and b/enrol/manual/amd/build/quickenrolment.min.js differ
index c2f7a3b..6f359b3 100644 (file)
@@ -31,6 +31,13 @@ define(['core/templates',
        ],
        function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment) {
 
+    /** @type {Object} The list of selectors for the quick enrolment modal. */
+    var SELECTORS = {
+        COHORTSELECT: "#id_cohortlist",
+        TRIGGERBUTTONS: ".enrolusersbutton.enrol_manual_plugin [type='submit']",
+        UNWANTEDHIDDENFIELDS: ":input[value='_qf__force_multiselect_submission']"
+    };
+
     /**
      * Constructor
      *
@@ -57,41 +64,46 @@ define(['core/templates',
      * @private
      */
     QuickEnrolment.prototype.initModal = function() {
-        var triggerButtons = $('.enrolusersbutton.enrol_manual_plugin [type="submit"]');
+        var triggerButtons = $(SELECTORS.TRIGGERBUTTONS);
+
+        $.when(
+            Str.get_strings([
+                {key: 'enroluserscohorts', component: 'enrol_manual'},
+                {key: 'enrolusers', component: 'enrol_manual'},
+            ]),
+            ModalFactory.create({
+                type: ModalFactory.types.SAVE_CANCEL,
+                large: true,
+            }, triggerButtons)
+        )
+        .then(function(strings, modal) {
+            this.modal = modal;
 
-        var stringsPromise = Str.get_strings([
-            {key: 'enroluserscohorts', component: 'enrol_manual'},
-            {key: 'enrolusers', component: 'enrol_manual'},
-        ]);
+            modal.setTitle(strings[1]);
+            modal.setSaveButtonText(strings[1]);
 
-        var titlePromise = stringsPromise.then(function(strings) {
-            return strings[1];
-        });
+            modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
+            modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
 
-        var buttonPromise = stringsPromise.then(function(strings) {
-            return strings[0];
-        });
+            // We want the reset the form every time it is opened.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                modal.setBody('');
+            });
 
-        return ModalFactory.create({
-            type: ModalFactory.types.SAVE_CANCEL,
-            large: true,
-            title: titlePromise,
-            body: this.getBody()
-        }, triggerButtons)
-        .then(function(modal) {
-            this.modal = modal;
+            modal.getRoot().on(ModalEvents.shown, function() {
+                var bodyPromise = this.getBody();
+                bodyPromise.then(function(html) {
+                    var stringIndex = $(html).find(SELECTORS.COHORTSELECT).length ? 0 : 1;
+                    modal.setSaveButtonText(strings[stringIndex]);
 
-            this.modal.setSaveButtonText(buttonPromise);
+                    return;
+                })
+                .fail(Notification.exception);
 
-            // We want the reset the form every time it is opened.
-            this.modal.getRoot().on(ModalEvents.hidden, function() {
-                this.modal.setBody(this.getBody());
+                modal.setBody(bodyPromise);
             }.bind(this));
 
-            this.modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
-            this.modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
-
-            return modal;
+            return;
         }.bind(this))
         .fail(Notification.exception);
     };
@@ -123,8 +135,8 @@ define(['core/templates',
 
         // Before send the data through AJAX, we need to parse and remove some unwanted hidden fields.
         // This hidden fields are added automatically by mforms and when it reaches the AJAX we get an error.
-        var hidden = form.find(':input[value="_qf__force_multiselect_submission"]');
-        hidden.each(function () {
+        var hidden = form.find(SELECTORS.UNWANTEDHIDDENFIELDS);
+        hidden.each(function() {
             this.remove();
         });
 
index 089810e..c34d9b2 100644 (file)
@@ -102,8 +102,24 @@ class enrol_manual_enrol_users_form extends moodleform {
         );
         $mform->addElement('autocomplete', 'userlist', get_string('selectusers', 'enrol_manual'), array(), $options);
 
-        $options = ['contextid' => $context->id, 'multiple' => true];
-        $mform->addElement('cohort', 'cohortlist', get_string('selectcohorts', 'enrol_manual'), $options);
+        // Confirm the user can search for cohorts before displaying select.
+        if (has_capability('moodle/cohort:manage', $context) || has_capability('moodle/cohort:view', $context)) {
+            // Check to ensure there is at least one visible cohort before displaying the select box.
+            // Ideally it would be better to call external_api::call_external_function('core_cohort_search_cohorts')
+            // (which is used to populate the select box) instead of duplicating logic but there is an issue with globals
+            // being borked (in this case $PAGE) when combining the usage of fragments and call_external_function().
+            require_once($CFG->dirroot . '/cohort/lib.php');
+            $availablecohorts = cohort_get_cohorts($context->id, 0, 1, '');
+            $availablecohorts = $availablecohorts['cohorts'];
+            if (!($context instanceof context_system)) {
+                $availablecohorts = array_merge($availablecohorts,
+                    cohort_get_available_cohorts($context, COHORT_ALL, 0, 1, ''));
+            }
+            if (!empty($availablecohorts)) {
+                $options = ['contextid' => $context->id, 'multiple' => true];
+                $mform->addElement('cohort', 'cohortlist', get_string('selectcohorts', 'enrol_manual'), $options);
+            }
+        }
 
         $roles = get_assignable_roles($context);
         $mform->addElement('select', 'roletoassign', get_string('assignrole', 'enrol_manual'), $roles);
index ca24f1d..1cde57b 100644 (file)
@@ -100,7 +100,8 @@ class behat_enrol extends behat_base {
             // We have a div here, not a tr.
             $this->execute('behat_forms::i_set_the_field_to', array(get_string('selectusers', 'enrol_manual'), $userfullname));
 
-            $this->execute("behat_forms::press_button", get_string('enroluserscohorts', 'enrol_manual'));
+            $enrolusers = get_string('enrolusers', 'enrol_manual');
+            $this->execute('behat_general::i_click_on_in_the', [$enrolusers, 'button', $enrolusers, 'dialogue']);
 
         } else {
             $this->execute('behat_forms::i_set_the_field_to', array(get_string('assignrole', 'role'), $rolename));
index b912982..c7b2559 100644 (file)
@@ -159,3 +159,7 @@ privacy,core_hub
 privacy_help,core_hub
 configloginhttps,core_admin
 loginhttps,core_admin
+groupaddnewnote,core_notes
+selectnotestate,core_notes
+extendenrol,core
+groupextendenrol,core
index 64a7588..91eac47 100644 (file)
@@ -111,6 +111,10 @@ $string['send'] = 'Send';
 $string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"';
 $string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}';
 $string['sendmessage'] = 'Send message';
+$string['sendbulkmessage'] = 'Send message to {$a} people';
+$string['sendbulkmessagesingle'] = 'Send message to 1 person';
+$string['sendbulkmessagesent'] = 'Message sent to {$a} people';
+$string['sendbulkmessagesentsingle'] = 'Message sent to 1 person';
 $string['sendmessageto'] = 'Send message to {$a}';
 $string['sendmessagetopopup'] = 'Send message to {$a} - new window';
 $string['settings'] = 'Settings';
index 71e29cd..f1eb252 100644 (file)
@@ -793,7 +793,6 @@ $string['expand'] = 'Expand';
 $string['expandall'] = 'Expand all';
 $string['expandcategory'] = 'Expand {$a}';
 $string['explanation'] = 'Explanation';
-$string['extendenrol'] = 'Extend enrolment (individual)';
 $string['extendperiod'] = 'Extended period';
 $string['failedloginattempts'] = '{$a->attempts} failed logins since your last login';
 $string['feedback'] = 'Feedback';
@@ -904,7 +903,6 @@ $string['gravatarenabled'] = '<a href="http://www.gravatar.com/">Gravatar</a> ha
 $string['group'] = 'Group';
 $string['groupadd'] = 'Add new group';
 $string['groupaddusers'] = 'Add selected to group';
-$string['groupextendenrol'] = 'Extend enrolment (common)';
 $string['groupfor'] = 'for group';
 $string['groupinfo'] = 'Info about selected group';
 $string['groupinfoedit'] = 'Edit group settings';
@@ -2165,3 +2163,5 @@ $string['sectionusedefaultname'] = 'Use default section name';
 
 // Deprecated since Moodle 3.4.
 $string['publish'] = 'Publish';
+$string['extendenrol'] = 'Extend enrolment (individual)';
+$string['groupextendenrol'] = 'Extend enrolment (common)';
index 1146eac..c2b388d 100644 (file)
  */
 
 $string['addnewnote'] = 'Add a new note';
+$string['addbulknote'] = 'Add a new note to {$a} people';
+$string['addbulknotesingle'] = 'Add a new note to 1 person';
+$string['addbulknotedone'] = 'Note added to {$a} people';
+$string['addbulknotedonesingle'] = 'Note added to 1 person';
 $string['addnewnoteselect'] = 'Select users to write notes about';
 $string['bynameondate'] = 'by {$a->name} - {$a->date}';
 $string['configenablenotes'] = 'Enable storing of notes about individual users.';
@@ -39,7 +43,6 @@ $string['eventnotecreated'] = 'Note created';
 $string['eventnoteupdated'] = 'Note updated';
 $string['eventnotedeleted'] = 'Note deleted';
 $string['eventnotesviewed'] = 'Notes viewed';
-$string['groupaddnewnote'] = 'Add a common note';
 $string['invalidid'] = 'Invalid note ID specified';
 $string['invaliduserid'] = 'Invalid user id: {$a}';
 $string['myprofileownnotes'] = 'My notes';
@@ -61,8 +64,10 @@ $string['publishstate_help'] = 'A note\'s context determines who can see the not
 * Personal - The note will be visible only to you
 * Course - The note will be visible to teachers in this course
 * Site - The note will be visible to teachers in all courses';
-$string['selectnotestate'] = "Select note state";
 $string['site'] = 'site';
 $string['sitenotes'] = 'Site notes';
 $string['unknown'] = 'unknown';
 
+// Deprecated since Moodle 3.4
+$string['groupaddnewnote'] = 'Add a common note';
+$string['selectnotestate'] = "Select note state";
index fa5980b..f105225 100644 (file)
@@ -456,7 +456,7 @@ $string['useshowadvancedtochange'] = 'Use \'Show advanced\' to change';
 $string['viewingdefinitionofrolex'] = 'Viewing the definition of role \'{$a}\'';
 $string['viewrole'] = 'View role details';
 $string['webservice:createtoken'] = 'Create a web service token';
-$string['webservice:managealltokens'] = 'Manage all users\' web services';
+$string['webservice:managealltokens'] = 'Manage all users\' web service tokens';
 $string['webservice:createmobiletoken'] = 'Create a web service token for mobile access';
 $string['whydoesuserhavecap'] = 'Why does {$a->fullname} have capability {$a->capability} in context {$a->context}?';
 $string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability {$a->capability} in context {$a->context}?';
index 30ec60f..5666dc3 100644 (file)
Binary files a/lib/amd/build/auto_rows.min.js and b/lib/amd/build/auto_rows.min.js differ
index 04cb7e1..d041bf7 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js and b/lib/amd/build/modal_factory.min.js differ
index 1532ad2..ab7a876 100644 (file)
@@ -42,6 +42,7 @@ define(['jquery'], function($) {
      */
     var calculateRows = function(element) {
         var currentRows = element.attr('rows');
+        var minRows = element.data('min-rows');
         var maxRows = element.attr('data-max-rows');
 
         var height = element.height();
@@ -58,13 +59,38 @@ define(['jquery'], function($) {
         // based on the row attribute.
         element.css('height', '');
 
-        if (maxRows && rows >= maxRows) {
+        if (rows < minRows) {
+            return minRows;
+        } else if (maxRows && rows >= maxRows) {
             return maxRows;
         } else {
             return rows;
         }
     };
 
+    /**
+     * Listener for change events to trigger resizing of the element.
+     *
+     * @method changeListener
+     * @param {Event} e The triggered event.
+     * @private
+     */
+    var changeListener = function(e) {
+        var element = $(e.target);
+        var minRows = element.data('min-rows');
+        var currentRows = element.attr('rows');
+
+        if (typeof minRows === "undefined") {
+            element.data('min-rows', currentRows);
+        }
+        var rows = calculateRows(element);
+
+        if (rows != currentRows) {
+            element.attr('rows', rows);
+            element.trigger(EVENTS.ROW_CHANGE);
+        }
+    };
+
     /**
      * Add the event listeners for all text areas within the given element.
      *
@@ -73,16 +99,11 @@ define(['jquery'], function($) {
      * @public
      */
     var init = function(root) {
-        $(root).on('input propertychange', SELECTORS.ELEMENT, function(e) {
-            var element = $(e.target);
-            var currentRows = element.attr('rows');
-            var rows = calculateRows(element);
-
-            if (rows != currentRows) {
-                element.attr('rows', rows);
-                $(root).trigger(EVENTS.ROW_CHANGE);
-            }
-        });
+        if ($(root).data('auto-rows')) {
+            $(root).on('input propertychange', changeListener.bind(this));
+        } else {
+            $(root).on('input propertychange', SELECTORS.ELEMENT, changeListener.bind(this));
+        }
     };
 
     return /** @module core/auto_rows */ {
index 770ecdf..29acb94 100644 (file)
@@ -63,12 +63,15 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
      */
     var setUpTrigger = function(modalPromise, triggerElement) {
         if (typeof triggerElement != 'undefined') {
+            // The element that actually shows the modal.
+            var actualTriggerElement = null;
             if (Array.isArray(triggerElement)) {
                 var selector = triggerElement[1];
                 triggerElement = triggerElement[0];
 
                 CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
                 triggerElement.on(CustomEvents.events.activate, selector, function(e, data) {
+                    actualTriggerElement = e.currentTarget;
                     modalPromise.then(function(modal) {
                         modal.show();
 
@@ -79,6 +82,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
             } else {
                 CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
                 triggerElement.on(CustomEvents.events.activate, function(e, data) {
+                    actualTriggerElement = e.currentTarget;
                     modalPromise.then(function(modal) {
                         modal.show();
 
@@ -90,7 +94,10 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
 
             modalPromise.then(function(modal) {
                 modal.getRoot().on(ModalEvents.hidden, function() {
-                    triggerElement.focus();
+                    // Focus on the trigger element that actually launched the modal.
+                    if (actualTriggerElement !== null) {
+                        actualTriggerElement.focus();
+                    }
                 });
 
                 return modal;
index 260c42a..6a7a562 100644 (file)
@@ -107,7 +107,10 @@ class iCalendar_property {
         $valarray = explode('\\,', $this->value);
 
         // Undo transparent formatting
-        $replace_function = create_function('$a', 'return rfc2445_undo_value_formatting($a, '.$this->val_type.');');
+        $valtype = $this->val_type;
+        $replace_function = function($a) use ($valtype) {
+            return rfc2445_undo_value_formatting($a, $valtype);
+        };
         $valarray = array_map($replace_function, $valarray);
 
         // Now, if this property cannot have multiple values, don't return as an array
index a5cfdd5..f95711a 100644 (file)
@@ -25,3 +25,4 @@ Changelog
 6/ MDL-49032: fixed rfc2445_fold() to fix incorrect RFC2445_WSP definition (16 Sep 2015)
 7/ added timestamp_to_date function to support zero duration events (16 Sept 2015)
 8/ Updated \iCalendar_event::invariant_holds() to allow for same dtstart and dtend timestamps (13 July 2017)
+9/ MDL-60391: replace create_function() with lambda function for PHP 7.2 compatibility (13 Oct 2017)
\ No newline at end of file
index 4b2c650..6749e95 100644 (file)
@@ -79,11 +79,17 @@ class calendar_subscription_created extends base
      * @return \moodle_url
      */
     public function get_url() {
-        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
-            return new \moodle_url('calendar/managesubscriptions.php');
-        } else {
-            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        $params = [];
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            $params['course'] = $this->other['courseid'];
+            if ($this->other['eventtype'] == 'group' && isset($this->other['groupid'])) {
+                $params['group'] = $this->other['groupid'];
+            }
         }
+        if ($this->other['eventtype'] == 'category' && isset($this->other['categoryid'])) {
+            $params['category'] = $this->other['categoryid'];
+        }
+        return new \moodle_url('/calendar/managesubscriptions.php', $params);
     }
 
     /**
@@ -103,8 +109,16 @@ class calendar_subscription_created extends base
         if (!isset($this->other['eventtype'])) {
             throw new \coding_exception('The \'eventtype\' value must be set in other.');
         }
-        if (!isset($this->other['courseid'])) {
-            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+            if ($this->other['eventtype'] == 'group' && !isset($this->other['groupid'])) {
+                throw new \coding_exception('The \'groupid\' value must be set in other.');
+            }
+        }
+        if ($this->other['eventtype'] == 'category' && !isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' value must be set in other.');
         }
     }
 
index 0f7ab2a..b3bbdca 100644 (file)
@@ -78,11 +78,26 @@ class calendar_subscription_deleted extends base
      * @return \moodle_url
      */
     public function get_url() {
-        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
-            return new \moodle_url('calendar/managesubscriptions.php');
+        $params = [];
+        if (isset($this->other['eventtype'])) {
+            if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+                $params['course'] = $this->other['courseid'];
+                if ($this->other['eventtype'] == 'group' && isset($this->other['groupid'])) {
+                    $params['group'] = $this->other['groupid'];
+                }
+            }
+            if ($this->other['eventtype'] == 'category' && isset($this->other['categoryid'])) {
+                $params['category'] = $this->other['categoryid'];
+            }
         } else {
-            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+            // This is a legacy event.
+            // Prior to specification of the eventtype there were only two params.
+            if (($this->other['courseid'] != SITEID) && ($this->other['courseid'] != 0)) {
+                $params['course'] = $this->other['courseid'];
+            }
         }
+        return new \moodle_url('/calendar/managesubscriptions.php', $params);
+
     }
 
     /**
@@ -99,8 +114,19 @@ class calendar_subscription_deleted extends base
         if (!isset($this->objectid)) {
             throw new \coding_exception('The \'objectid\' must be set.');
         }
-        if (!isset($this->other['courseid'])) {
-            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        if (!isset($this->other['eventtype'])) {
+            throw new \coding_exception('The \'eventtype\' value must be set in other.');
+        }
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+            if ($this->other['eventtype'] == 'group' && !isset($this->other['groupid'])) {
+                throw new \coding_exception('The \'groupid\' value must be set in other.');
+            }
+        }
+        if ($this->other['eventtype'] == 'category' && !isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' value must be set in other.');
         }
     }
 
index eb61033..a8d6d03 100644 (file)
@@ -79,11 +79,17 @@ class calendar_subscription_updated extends base
      * @return \moodle_url
      */
     public function get_url() {
-        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
-            return new \moodle_url('calendar/managesubscriptions.php');
-        } else {
-            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        $params = [];
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            $params['course'] = $this->other['courseid'];
+            if ($this->other['eventtype'] == 'group' && isset($this->other['groupid'])) {
+                $params['group'] = $this->other['groupid'];
+            }
         }
+        if ($this->other['eventtype'] == 'category' && isset($this->other['categoryid'])) {
+            $params['category'] = $this->other['categoryid'];
+        }
+        return new \moodle_url('/calendar/managesubscriptions.php', $params);
     }
 
     /**
@@ -103,8 +109,16 @@ class calendar_subscription_updated extends base
         if (!isset($this->other['eventtype'])) {
             throw new \coding_exception('The \'eventtype\' value must be set in other.');
         }
-        if (!isset($this->other['courseid'])) {
-            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+            if ($this->other['eventtype'] == 'group' && !isset($this->other['groupid'])) {
+                throw new \coding_exception('The \'groupid\' value must be set in other.');
+            }
+        }
+        if ($this->other['eventtype'] == 'category' && !isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' value must be set in other.');
         }
     }
 
index bbf96b4..03e5684 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20170929" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20171026" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
index 34425f6..3bef679 100644 (file)
@@ -1034,6 +1034,7 @@ $functions = array(
         'classpath' => 'notes/externallib.php',
         'description' => 'Create notes',
         'type' => 'write',
+        'ajax' => true,
         'capabilities' => 'moodle/notes:manage',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
index 50599a7..09f65e7 100644 (file)
@@ -2793,5 +2793,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017102100.01);
     }
 
+    if ($oldversion < 2017110300.01) {
+
+        // Define field categoryid to be added to event_subscriptions.
+        $table = new xmldb_table('event_subscriptions');
+        $field = new xmldb_field('categoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'url');
+
+        // Conditionally launch add field categoryid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017110300.01);
+    }
+
     return true;
 }
index 6f4ee64..fa4c6c2 100644 (file)
@@ -149,7 +149,8 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 
         // This should generate an exception.
-        $this->setExpectedExceptionRegexp('file_exception',
+        $this->expectException('file_exception');
+        $this->expectExceptionMessageRegExp(
             '/Can not create local file pool directories, please verify permissions in dataroot./');
 
         new file_system_filedir();
@@ -170,7 +171,8 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 
         // This should generate an exception.
-        $this->setExpectedExceptionRegexp('file_exception',
+        $this->expectException('file_exception');
+        $this->expectExceptionMessageRegExp(
             '/Can not create local file pool directories, please verify permissions in dataroot./');
 
         new file_system_filedir();
@@ -693,7 +695,8 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
         // Setup the filedir.
         $vfileroot = $this->setup_vfile_root();
 
-        $this->setExpectedExceptionRegexp('file_exception',
+        $this->expectException('file_exception');
+        $this->expectExceptionMessageRegExp(
             '/Cannot read file\. Either the file does not exist or there is a permission problem\./');
 
         $fs = new file_system_filedir();
@@ -785,10 +788,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
             ->chmod(0444)
             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 
-        $this->setExpectedException(
-            'file_exception',
-            "Can not create local file pool directories, please verify permissions in dataroot."
-        );
+        $this->expectException('file_exception');
+        $this->expectExceptionMessageRegExp(
+            "/Can not create local file pool directories, please verify permissions in dataroot./");
 
         // Attempt to add the file to the file pool.
         $fs = new file_system_filedir();
@@ -840,10 +842,9 @@ class core_files_file_system_filedir_testcase extends advanced_testcase {
             ->chmod(0444)
             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 
-        $this->setExpectedException(
-            'file_exception',
-            "Can not create local file pool directories, please verify permissions in dataroot."
-        );
+        $this->expectException('file_exception');
+        $this->expectExceptionMessageRegExp(
+            "/Can not create local file pool directories, please verify permissions in dataroot./");
 
         // Attempt to add the file to the file pool.
         $fs = new file_system_filedir();
index d7f2500..860b8af 100644 (file)
@@ -463,7 +463,7 @@ class core_files_zip_packer_testcase extends advanced_testcase implements file_p
             $result = $zip_archive->close();
         } catch (Exception $e) {
             // New PHP versions print PHP Warning.
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
             $this->assertContains('ZipArchive::close', $e->getMessage());
         }
         // This is crazy, but it shows how some PHP versions do return true.
@@ -472,7 +472,7 @@ class core_files_zip_packer_testcase extends advanced_testcase implements file_p
             $this->assertFalse($result);
         } catch (Exception $e) {
             // But others do insist into returning true (5.6.13...). Only can accept them.
-            $this->assertInstanceOf('PHPUnit_Framework_ExpectationFailedException', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\ExpectationFailedException', $e);
             $this->assertTrue($result);
         }
         $this->assertFileNotExists($archive);
index b0ca93e..864f23e 100644 (file)
@@ -38,7 +38,7 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
     /**
      * The required version of the python package that performs all calculations.
      */
-    const REQUIRED_PIP_PACKAGE_VERSION = '0.0.4';
+    const REQUIRED_PIP_PACKAGE_VERSION = '0.0.5';
 
     /**
      * The path to the Python bin.
index 5ad0bfe..3e88f50 100644 (file)
@@ -72,7 +72,7 @@ if (defined('CLI_SCRIPT')) {
 }
 define('CLI_SCRIPT', true);
 
-$phpunitversion = PHPUnit_Runner_Version::id();
+$phpunitversion = PHPUnit\Runner\Version::id();
 if ($phpunitversion === '@package_version@') {
     // library checked out from git, let's hope dev knows that 3.6.0 is required
 } else if (version_compare($phpunitversion, '3.6.0', 'lt')) {
@@ -80,10 +80,6 @@ if ($phpunitversion === '@package_version@') {
 }
 unset($phpunitversion);
 
-if (!include_once('PHPUnit/Extensions/Database/Autoload.php')) {
-    phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITEXTMISSING, 'phpunit/DbUnit');
-}
-
 define('NO_OUTPUT_BUFFERING', true);
 
 // only load CFG from config.php, stop ASAP in lib/setup.php
index ee05a32..a0ec589 100644 (file)
@@ -135,9 +135,9 @@ abstract class advanced_testcase extends base_testcase {
         // make sure test did not forget to close transaction
         if ($DB->is_transaction_started()) {
             self::resetAllData();
-            if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
-                or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
-                or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
+            if ($this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_PASSED
+                or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED
+                or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) {
                 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
             }
         }
@@ -147,20 +147,20 @@ abstract class advanced_testcase extends base_testcase {
      * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
      *
      * @param string $xmlFile
-     * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet
+     * @return PHPUnit\DbUnit\DataSet\FlatXmlDataSet
      */
     protected function createFlatXMLDataSet($xmlFile) {
-        return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile);
+        return new PHPUnit\DbUnit\DataSet\FlatXmlDataSet($xmlFile);
     }
 
     /**
      * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
      *
      * @param string $xmlFile
-     * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet
+     * @return PHPUnit\DbUnit\DataSet\XmlDataSet
      */
     protected function createXMLDataSet($xmlFile) {
-        return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile);
+        return new PHPUnit\DbUnit\DataSet\XmlDataSet($xmlFile);
     }
 
     /**
@@ -170,10 +170,10 @@ abstract class advanced_testcase extends base_testcase {
      * @param string $delimiter
      * @param string $enclosure
      * @param string $escape
-     * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet
+     * @return PHPUnit\DbUnit\DataSet\CsvDataSet
      */
     protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
-        $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape);
+        $dataSet = new PHPUnit\DbUnit\DataSet\CsvDataSet($delimiter, $enclosure, $escape);
         foreach($files as $table=>$file) {
             $dataSet->addTable($table, $file);
         }
@@ -195,10 +195,10 @@ abstract class advanced_testcase extends base_testcase {
      *
      * Note: it is usually better to use data generators
      *
-     * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset
+     * @param PHPUnit\DbUnit\DataSet\IDataSet $dataset
      * @return void
      */
-    protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) {
+    protected function loadDataSet(PHPUnit\DbUnit\DataSet\IDataSet $dataset) {
         global $DB;
 
         $structure = phpunit_util::get_tablestructure();
index d4148f9..c735be4 100644 (file)
@@ -33,7 +33,7 @@
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class phpunit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet {
+class phpunit_ArrayDataSet extends PHPUnit\DbUnit\DataSet\AbstractDataSet {
     /**
      * @var array
      */
@@ -58,8 +58,8 @@ class phpunit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractD
                 $columns = array_keys($firstrow);
             }
 
-            $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
-            $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
+            $metaData = new PHPUnit\DbUnit\DataSet\DefaultTableMetaData($tableName, $columns);
+            $table = new PHPUnit\DbUnit\DataSet\DefaultTable($metaData);
 
             foreach ($rows AS $row) {
                 if ($columnsInFirstRow) {
@@ -72,7 +72,7 @@ class phpunit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractD
     }
 
     protected function createIterator($reverse = FALSE) {
-        return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
+        return new PHPUnit\DbUnit\DataSet\DefaultTableIterator($this->tables, $reverse);
     }
 
     public function getTable($tableName) {
index 611512b..c39ec4a 100644 (file)
@@ -42,7 +42,7 @@
  * @copyright  2013 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class phpunit_autoloader implements PHPUnit_Runner_TestSuiteLoader {
+class phpunit_autoloader implements \PHPUnit\Runner\TestSuiteLoader {
     public function load($suiteClassName, $suiteClassFile = '') {
         global $CFG;
 
@@ -66,13 +66,13 @@ class phpunit_autoloader implements PHPUnit_Runner_TestSuiteLoader {
         }
 
         if ($suiteClassFile) {
-            PHPUnit_Util_Fileloader::checkAndLoad($suiteClassFile);
+            PHPUnit\Util\Fileloader::checkAndLoad($suiteClassFile);
             if (class_exists($suiteClassName, false)) {
                 $class = new ReflectionClass($suiteClassName);
                 return $class;
             }
 
-            throw new PHPUnit_Framework_Exception(
+            throw new PHPUnit\Framework\Exception(
                 sprintf("Class '%s' could not be found in '%s'.", $suiteClassName, $suiteClassFile)
             );
         }
@@ -155,7 +155,7 @@ class phpunit_autoloader implements PHPUnit_Runner_TestSuiteLoader {
             }
         }
 
-        throw new PHPUnit_Framework_Exception(
+        throw new PHPUnit\Framework\Exception(
             sprintf("Class '%s' could not be found in '%s'.", $suiteClassName, $suiteClassFile)
         );
     }
@@ -165,7 +165,7 @@ class phpunit_autoloader implements PHPUnit_Runner_TestSuiteLoader {
         // class name. Let's throw fatal error if there are more testcases in one file.
 
         $classes = get_declared_classes();
-        PHPUnit_Util_Fileloader::checkAndLoad($file);
+        PHPUnit\Util\Fileloader::checkAndLoad($file);
         $includePathFilename = stream_resolve_include_path($file);
         $loadedClasses = array_diff(get_declared_classes(), $classes);
 
@@ -174,7 +174,7 @@ class phpunit_autoloader implements PHPUnit_Runner_TestSuiteLoader {
         foreach ($loadedClasses as $loadedClass) {
             $class = new ReflectionClass($loadedClass);
 
-            if ($class->isSubclassOf('PHPUnit_Framework_TestCase') and !$class->isAbstract()) {
+            if ($class->isSubclassOf('PHPUnit\Framework\TestCase') and !$class->isAbstract()) {
                 if (realpath($includePathFilename) === realpath($class->getFileName())) {
                     $candidates[] = $loadedClass;
                 }
@@ -182,13 +182,13 @@ class phpunit_autoloader implements PHPUnit_Runner_TestSuiteLoader {
         }
 
         if (count($candidates) == 0) {
-            throw new PHPUnit_Framework_Exception(
+            throw new PHPUnit\Framework\Exception(
                 sprintf("File '%s' does not contain any test cases.", $file)
             );
         }
 
         if (count($candidates) > 1) {
-            throw new PHPUnit_Framework_Exception(
+            throw new PHPUnit\Framework\Exception(
                 sprintf("File '%s' contains multiple test cases: ".implode(', ', $candidates), $file)
             );
         }
index 8f78f4c..f24c562 100644 (file)
@@ -36,7 +36,7 @@
  * @copyright  2015 Blackboard (http://www.blackboard.com)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-abstract class base_testcase extends PHPUnit_Framework_TestCase {
+abstract class base_testcase extends PHPUnit\Framework\TestCase {
     // @codingStandardsIgnoreStart
     // Following code is legacy code from phpunit to support assertTag
     // and assertNotTag.
@@ -53,7 +53,7 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase {
      * @deprecated 3.0
      */
     public static function assertTag($matcher, $actual, $message = '', $ishtml = true) {
-        $dom = PHPUnit_Util_XML::load($actual, $ishtml);
+        $dom = PHPUnit\Util\XML::load($actual, $ishtml);
         $tags = self::findNodes($dom, $matcher, $ishtml);
         $matched = count($tags) > 0 && $tags[0] instanceof DOMNode;
         self::assertTrue($matched, $message);
@@ -71,7 +71,7 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase {
      * @deprecated 3.0
      */
     public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) {
-        $dom = PHPUnit_Util_XML::load($actual, $ishtml);
+        $dom = PHPUnit\Util\XML::load($actual, $ishtml);
         $tags = self::findNodes($dom, $matcher, $ishtml);
         $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode;
         self::assertFalse($matched, $message);
@@ -85,7 +85,7 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase {
      *
      * @return array
      *
-     * @throws PHPUnit_Framework_Exception
+     * @throws PHPUnit\Framework\Exception
      */
     public static function assertValidKeys(array $hash, array $validKeys) {
         $valids = array();
@@ -106,7 +106,7 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase {
         }
 
         if (!empty($unknown)) {
-            throw new PHPUnit_Framework_Exception(
+            throw new PHPUnit\Framework\Exception(
                 'Unknown key(s): ' . implode(', ', $unknown)
             );
         }
index fc5ac7a..9407b7a 100644 (file)
@@ -32,7 +32,7 @@
  * @copyright  2015 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit_Framework_Constraint_IsEqual {
+class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit\Framework\Constraint\IsEqual {
 
     /**
      * @var array $keys The list of exceptions.
@@ -41,7 +41,7 @@ class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit_Framewo
 
     /**
      * Add an exception for the named key to use a different comparison
-     * method. Any assertion provided by PHPUnit_Framework_Assert is
+     * method. Any assertion provided by PHPUnit\Framework\Assert is
      * acceptable.
      *
      * @param string $key The key to except.
@@ -65,13 +65,13 @@ class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit_Framewo
      * @param  string   $description        Additional information about the test
      * @param  bool     $shouldreturnesult  Whether to return a result or throw an exception
      * @return mixed
-     * @throws PHPUnit_Framework_ExpectationFailedException
+     * @throws PHPUnit\Framework\ExpectationFailedException
      */
     public function evaluate($other, $description = '', $shouldreturnesult = false) {
         foreach ($this->keys as $key => $comparison) {
             if (isset($other->$key) || isset($this->value->$key)) {
                 // One of the keys is present, therefore run the comparison.
-                PHPUnit_Framework_Assert::$comparison($this->value->$key, $other->$key);
+                PHPUnit\Framework\Assert::$comparison($this->value->$key, $other->$key);
 
                 // Unset the keys, otherwise the standard evaluation will take place.
                 unset($other->$key);
index 4a06814..a21ca51 100644 (file)
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
+class Hint_ResultPrinter extends PHPUnit\TextUI\ResultPrinter {
     public function __construct() {
         // ARRGH - PHPUnit does not give us commandline arguments or xml config, so let's hack hard!
         if (defined('DEBUG_BACKTRACE_PROVIDE_OBJECT')) {
             $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
-            if (isset($backtrace[2]['object']) and ($backtrace[2]['object'] instanceof PHPUnit_TextUI_Command)) {
+            if (isset($backtrace[2]['object']) and ($backtrace[2]['object'] instanceof PHPUnit\TextUI\Command)) {
                 list($verbose, $colors, $debug) = Hacky_TextUI_Command_reader::get_settings_hackery($backtrace[2]['object']);
                 parent::__construct(null, $verbose, $colors, $debug);
                 return;
@@ -48,7 +48,7 @@ class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
         parent::__construct(null, false, self::COLOR_DEFAULT, false);
     }
 
-    protected function printDefectTrace(PHPUnit_Framework_TestFailure $defect) {
+    protected function printDefectTrace(PHPUnit\Framework\TestFailure $defect) {
         global $CFG;
 
         parent::printDefectTrace($defect);
@@ -129,10 +129,10 @@ class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class Hacky_TextUI_Command_reader extends PHPUnit_TextUI_Command {
-    public static function get_settings_hackery(PHPUnit_TextUI_Command $toread) {
+class Hacky_TextUI_Command_reader extends PHPUnit\TextUI\Command {
+    public static function get_settings_hackery(PHPUnit\TextUI\Command $toread) {
         $arguments = $toread->arguments;
-        $config = PHPUnit_Util_Configuration::getInstance($arguments['configuration'])->getPHPUnitConfiguration();
+        $config = PHPUnit\Util\Configuration::getInstance($arguments['configuration'])->getPHPUnitConfiguration();
 
         $verbose = isset($config['verbose']) ? $config['verbose'] : false;
         $verbose = isset($arguments['verbose']) ? $arguments['verbose'] : $verbose;
index 240777a..d5cfd94 100644 (file)
@@ -621,7 +621,7 @@ class phpunit_util extends testing_util {
 
         foreach ($backtrace as $bt) {
             if (isset($bt['object']) and is_object($bt['object'])
-                    && $bt['object'] instanceof PHPUnit_Framework_TestCase) {
+                    && $bt['object'] instanceof PHPUnit\Framework\TestCase) {
                 $debug = new stdClass();
                 $debug->message = $message;
                 $debug->level   = $level;
index 1ffa321..7612ee3 100644 (file)
     <xs:attribute name="convertNoticesToExceptions" type="xs:boolean" default="true"/>
     <xs:attribute name="convertWarningsToExceptions" type="xs:boolean" default="true"/>
     <xs:attribute name="forceCoversAnnotation" type="xs:boolean" default="false"/>
-    <xs:attribute name="printerClass" type="xs:string" default="PHPUnit_TextUI_ResultPrinter"/>
+    <xs:attribute name="printerClass" type="xs:string" default="PHPUnit\TextUI\ResultPrinter"/>
     <xs:attribute name="printerFile" type="xs:anyURI"/>
     <xs:attribute name="processIsolation" type="xs:boolean" default="false"/>
     <xs:attribute name="stopOnError" type="xs:boolean" default="false"/>
     <xs:attribute name="timeoutForSmallTests" type="xs:integer" default="1"/>
     <xs:attribute name="timeoutForMediumTests" type="xs:integer" default="10"/>
     <xs:attribute name="timeoutForLargeTests" type="xs:integer" default="60"/>
-    <xs:attribute name="testSuiteLoaderClass" type="xs:string" default="PHPUnit_Runner_StandardTestSuiteLoader"/>
+    <xs:attribute name="testSuiteLoaderClass" type="xs:string" default="PHPUnit\Runner\StandardTestSuiteLoader"/>
     <xs:attribute name="testSuiteLoaderFile" type="xs:anyURI"/>
     <xs:attribute name="verbose" type="xs:boolean" default="false"/>
     <xs:attribute name="stderr" type="xs:boolean" default="false"/>
index a69477e..2aaa254 100644 (file)
@@ -223,7 +223,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         try {
             self::resetAllData(true);
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
         }
         $this->assertEquals(1, $DB->get_field('user', 'confirmed', array('id'=>2)));
 
@@ -234,7 +234,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         try {
             self::resetAllData(true);
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
             $this->assertContains('xx', $e->getMessage());
             $this->assertContains('admin', $e->getMessage());
             $this->assertContains('rolesactive', $e->getMessage());
@@ -276,7 +276,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         try {
             self::resetAllData(true);
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
             $this->assertEquals(1, $SITE->id);
             $this->assertSame($SITE, $COURSE);
             $this->assertSame($SITE, $COURSE);
@@ -287,7 +287,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         try {
             self::resetAllData(true);
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
             $this->assertEquals(0, $USER->id);
         }
     }
@@ -377,7 +377,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
             $this->assertTimeCurrent(time()+10);
             $this->fail('Failed assert expected');
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_ExpectationFailedException', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\ExpectationFailedException', $e);
         }
 
         try {
@@ -385,7 +385,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
             $this->assertTimeCurrent(time()-10);
             $this->fail('Failed assert expected');
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_ExpectationFailedException', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\ExpectationFailedException', $e);
         }
     }
 
@@ -600,19 +600,19 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         try {
             $this->setTimezone('Pacific/Auckland', '');
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
         }
 
         try {
             $this->setTimezone('Pacific/Auckland', 'xxxx');
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
         }
 
         try {
             $this->setTimezone('Pacific/Auckland', null);
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
         }
 
     }
@@ -637,7 +637,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         try {
             self::resetAllData(true);
         } catch (Exception $e) {
-            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+            $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
         }
 
         if ($CFG->ostype === 'WINDOWS') {
index a2f2c2f..a2b0df2 100644 (file)
@@ -851,7 +851,7 @@ class core_event_testcase extends advanced_testcase {
     }
 
     /**
-     * @expectedException PHPUnit_Framework_Error_Notice
+     * @expectedException PHPUnit\Framework\Error\Notice
      */
     public function test_context_not_used() {
         $event = \core_tests\event\context_used_in_event::create(array('other' => array('sample' => 1, 'xx' => 10)));
index 257f82b..faa63fb 100644 (file)
@@ -1824,7 +1824,7 @@ class core_moodlelib_testcase extends advanced_testcase {
     }
 
     /**
-     * @expectedException PHPUnit_Framework_Error_Warning
+     * @expectedException PHPUnit\Framework\Error\Warning
      */
     public function test_get_string_limitation() {
         // This is one of the limitations to the lang_string class. It can't be
index 8bc1202..8b32e75 100644 (file)
@@ -55,6 +55,12 @@ class core_session_redis_testcase extends advanced_testcase {
         if (!defined('TEST_SESSION_REDIS_HOST')) {
             $this->markTestSkipped('Session test server not set. define: TEST_SESSION_REDIS_HOST');
         }
+        $version = phpversion('Redis');
+        if (!$version) {
+            $this->markTestSkipped('Redis extension version missing');
+        } else if (version_compare($version, '2.0') <= 0) {
+            $this->markTestSkipped('Redis extension version must be at least 2.0: now running "' . $version . '"');
+        }
 
         $this->resetAfterTest();
 
index 31b99d1..f8ced49 100644 (file)
@@ -188,16 +188,16 @@ class core_statslib_testcase extends advanced_testcase {
         static $replacements = null;
 
         $raw   = $this->createXMLDataSet($file);
-        $clean = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($raw);
+        $clean = new PHPUnit\DbUnit\DataSet\ReplacementDataSet($raw);
 
         foreach ($this->replacements as $placeholder => $value) {
             $clean->addFullReplacement($placeholder, $value);
         }
 
-        $logs = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($clean);
+        $logs = new PHPUnit\DbUnit\DataSet\Filter($clean);
         $logs->addIncludeTables(array('log'));
 
-        $stats = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($clean);
+        $stats = new PHPUnit\DbUnit\DataSet\Filter($clean);
         $stats->addIncludeTables(array('stats_daily', 'stats_user_daily'));
 
         return array($logs, $stats);
index 4716080..ae480fd 100644 (file)
@@ -3275,8 +3275,7 @@ class text_progress_trace extends progress_trace {
      * @return void Output is echo'd
      */
     public function output($message, $depth = 0) {
-        echo str_repeat('  ', $depth), $message, "\n";
-        flush();
+        mtrace(str_repeat('  ', $depth) . $message);
     }
 }
 
index f5a5203..aea9067 100644 (file)
@@ -53,4 +53,14 @@ abstract class activity_base extends \core_analytics\local\indicator\community_o
         // We need the grade to be released to the student to consider that feedback has been provided.
         return true;
     }
+
+    /**
+     * Returns the name of the field that controls activity availability.
+     *
+     * @return null|string
+     */
+    protected function get_timeclose_field() {
+        return 'duedate';
+    }
+
 }
index c6b0121..5eeccf3 100644 (file)
@@ -1946,31 +1946,22 @@ function mod_assign_core_calendar_event_action_shows_item_count(calendar_event $
  *     [1506741172, 'The due date must be before the cutoff date']
  * ]
  *
+ * If the event does not have a valid timestart range then [false, false] will
+ * be returned.
+ *
  * @param calendar_event $event The calendar event to get the time range for
- * @param stdClass|null $instance The module instance to get the range from
+ * @param stdClass $instance The module instance to get the range from
+ * @return array
  */
-function mod_assign_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance = null) {
-    global $CFG, $DB;
+function mod_assign_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
+    global $CFG;
 
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
 
-    if (!$instance) {
-        $instance = $DB->get_record('assign', ['id' => $event->instance]);
-    }
-
-    $coursemodule = get_coursemodule_from_instance('assign',
-                                         $event->instance,
-                                         $event->courseid,
-                                         false,
-                                         MUST_EXIST);
-
-    if (empty($coursemodule)) {
-        // If we don't have a course module yet then it likely means
-        // the activity is still being set up. In this case there is
-        // nothing for us to do anyway.
-        return;
-    }
-
+    $courseid = $event->courseid;
+    $modulename = $event->modulename;
+    $instanceid = $event->instance;
+    $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
     $context = context_module::instance($coursemodule->id);
     $assign = new assign($context, null, null);
     $assign->set_instance($instance);
@@ -1978,46 +1969,15 @@ function mod_assign_core_calendar_get_valid_event_timestart_range(\calendar_even
     return $assign->get_valid_calendar_event_timestart_range($event);
 }
 
-/**
- * This function will check that the given event is valid for it's
- * corresponding assign module instance.
- *
- * An exception is thrown if the event fails validation.
- *
- * @throws \moodle_exception
- * @param \calendar_event $event
- * @return bool
- */
-function mod_assign_core_calendar_validate_event_timestart(\calendar_event $event) {
-    global $DB;
-
-    if (!isset($event->instance)) {
-        return;
-    }
-
-    // We need to read from the DB directly because course module may
-    // currently be getting created so it won't be in mod info yet.
-    $instance = $DB->get_record('assign', ['id' => $event->instance], '*', MUST_EXIST);
-    $timestart = $event->timestart;
-    list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
-
-    if ($min && $timestart < $min[0]) {
-        throw new \moodle_exception($min[1]);
-    }
-
-    if ($max && $timestart > $max[0]) {
-        throw new \moodle_exception($max[1]);
-    }
-}
-
 /**
  * This function will update the assign module according to the
  * event that has been modified.
  *
  * @throws \moodle_exception
  * @param \calendar_event $event
+ * @param stdClass $instance The module instance to get the range from
  */
-function mod_assign_core_calendar_event_timestart_updated(\calendar_event $event) {
+function mod_assign_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $instance) {
     global $CFG, $DB;
 
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
@@ -2026,19 +1986,19 @@ function mod_assign_core_calendar_event_timestart_updated(\calendar_event $event
         return;
     }
 
-    $coursemodule = get_coursemodule_from_instance('assign',
-                                         $event->instance,
-                                         $event->courseid,
-                                         false,
-                                         MUST_EXIST);