MDL-67613 availability_completion: add previous activity condition
authorFerran Recio <ferran@moodle.com>
Mon, 23 Dec 2019 15:38:50 +0000 (16:38 +0100)
committerFerran Recio <ferran@moodle.com>
Wed, 6 May 2020 08:56:47 +0000 (10:56 +0200)
availability/condition/completion/classes/condition.php
availability/condition/completion/classes/frontend.php
availability/condition/completion/db/caches.php [new file with mode: 0644]
availability/condition/completion/lang/en/availability_completion.php
availability/condition/completion/tests/behat/availability_completion_previous.feature [new file with mode: 0644]
availability/condition/completion/tests/condition_test.php
availability/condition/completion/version.php
availability/tests/fixtures/mock_info_module.php [new file with mode: 0644]
availability/tests/fixtures/mock_info_section.php [new file with mode: 0644]

index 11f86c8..b4310e8 100644 (file)
 
 namespace availability_completion;
 
+use cache;
+use core_availability\info;
+use core_availability\info_module;
+use core_availability\info_section;
+use stdClass;
+
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir . '/completionlib.php');
@@ -36,12 +42,25 @@ require_once($CFG->libdir . '/completionlib.php');
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class condition extends \core_availability\condition {
+
+    /** @var int previous module cm value used to calculate relative completions */
+    public const OPTION_PREVIOUS = -1;
+
     /** @var int ID of module that this depends on */
     protected $cmid;
 
+    /** @var array IDs of the current module and section */
+    protected $selfids;
+
     /** @var int Expected completion type (one of the COMPLETE_xx constants) */
     protected $expectedcompletion;
 
+    /** @var array Array of previous cmids used to calculate relative completions */
+    protected $modfastprevious = array();
+
+    /** @var array Array of cmids previous to each course section */
+    protected $sectionfastprevious = array();
+
     /** @var array Array of modules used in these conditions for course */
     protected static $modsusedincondition = array();
 
@@ -58,7 +77,6 @@ class condition extends \core_availability\condition {
         } else {
             throw new \coding_exception('Missing or invalid ->cm for completion condition');
         }
-
         // Get expected completion.
         if (isset($structure->e) && in_array($structure->e,
                 array(COMPLETION_COMPLETE, COMPLETION_INCOMPLETE,
@@ -69,7 +87,12 @@ class condition extends \core_availability\condition {
         }
     }
 
-    public function save() {
+    /**
+     * Saves tree data back to a structure object.
+     *
+     * @return stdClass Structure object (ready to be made into JSON format)
+     */
+    public function save(): stdClass {
         return (object)array('type' => 'completion',
                 'cm' => $this->cmid, 'e' => $this->expectedcompletion);
     }
@@ -84,22 +107,38 @@ class condition extends \core_availability\condition {
      * @param int $expectedcompletion Expected completion value (COMPLETION_xx)
      * @return stdClass Object representing condition
      */
-    public static function get_json($cmid, $expectedcompletion) {
+    public static function get_json(int $cmid, int $expectedcompletion): stdClass {
         return (object)array('type' => 'completion', 'cm' => (int)$cmid,
                 'e' => (int)$expectedcompletion);
     }
 
-    public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
+    /**
+     * Determines whether a particular item is currently available
+     * according to this availability condition.
+     *
+     * @see \core_availability\tree_node\update_after_restore
+     *
+     * @param bool $not Set true if we are inverting the condition
+     * @param info $info Item we're checking
+     * @param bool $grabthelot Performance hint: if true, caches information
+     *   required for all course-modules, to make the front page and similar
+     *   pages work more quickly (works only for current user)
+     * @param int $userid User ID to check availability for
+     * @return bool True if available
+     */
+    public function is_available($not, info $info, $grabthelot, $userid): bool {
+        list($selfcmid, $selfsectionid) = $this->get_selfids($info);
+        $cmid = $this->get_cmid($info->get_course(), $selfcmid, $selfsectionid);
         $modinfo = $info->get_modinfo();
         $completion = new \completion_info($modinfo->get_course());
-        if (!array_key_exists($this->cmid, $modinfo->cms)) {
+        if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) {
             // If the cmid cannot be found, always return false regardless
             // of the condition or $not state. (Will be displayed in the
             // information message.)
             $allow = false;
         } else {
             // The completion system caches its own data so no caching needed here.
-            $completiondata = $completion->get_data((object)array('id' => $this->cmid),
+            $completiondata = $completion->get_data((object)array('id' => $cmid),
                     $grabthelot, $userid, $modinfo);
 
             $allow = true;
@@ -128,6 +167,134 @@ class condition extends \core_availability\condition {
         return $allow;
     }
 
+    /**
+     * Return current item IDs (cmid and sectionid).
+     *
+     * @param info $info
+     * @return int[] with [0] => cmid/null, [1] => sectionid/null
+     */
+    public function get_selfids(info $info): array {
+        if (isset($this->selfids)) {
+            return $this->selfids;
+        }
+        if ($info instanceof info_module) {
+            $cminfo = $info->get_course_module();
+            if (!empty($cminfo->id)) {
+                $this->selfids = array($cminfo->id, null);
+                return $this->selfids;
+            }
+        }
+        if ($info instanceof info_section) {
+            $section = $info->get_section();
+            if (!empty($section->id)) {
+                $this->selfids = array(null, $section->id);
+                return $this->selfids;
+            }
+
+        }
+        return array(null, null);
+    }
+
+    /**
+     * Get the cmid referenced in the access restriction.
+     *
+     * @param stdClass $course course object
+     * @param int|null $selfcmid current course-module ID or null
+     * @param int|null $selfsectionid current course-section ID or null
+     * @return int|null cmid or null if no referenced cm is found
+     */
+    public function get_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int {
+        if ($this->cmid > 0) {
+            return $this->cmid;
+        }
+        // If it's a relative completion, load fast browsing.
+        if ($this->cmid == self::OPTION_PREVIOUS) {
+            $prevcmid = $this->get_previous_cmid($course, $selfcmid, $selfsectionid);
+            if ($prevcmid) {
+                return $prevcmid;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the previous CM ID of an specific course-module or course-section.
+     *
+     * @param stdClass $course course object
+     * @param int|null $selfcmid course-module ID or null
+     * @param int|null $selfsectionid course-section ID or null
+     * @return int|null
+     */
+    private function get_previous_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int {
+        $this->load_course_structure($course);
+        if (isset($this->modfastprevious[$selfcmid])) {
+            return $this->modfastprevious[$selfcmid];
+        }
+        if (isset($this->sectionfastprevious[$selfsectionid])) {
+            return $this->sectionfastprevious[$selfsectionid];
+        }
+        return null;
+    }
+
+    /**
+     * Loads static information about a course elements previous activities.
+     *
+     * Populates two variables:
+     *   - $this->sectionprevious[] course-module previous to a cmid
+     *   - $this->sectionfastprevious[] course-section previous to a cmid
+     *
+     * @param stdClass $course course object
+     */
+    private function load_course_structure(stdClass $course): void {
+        // If already loaded we don't need to do anything.
+        if (empty($this->modfastprevious)) {
+            $previouscache = cache::make('availability_completion', 'previous_cache');
+            $this->modfastprevious = $previouscache->get("mod_{$course->id}");
+            $this->sectionfastprevious = $previouscache->get("sec_{$course->id}");
+        }
+
+        if (!empty($this->modfastprevious)) {
+            return;
+        }
+
+        if (empty($this->modfastprevious)) {
+            $this->modfastprevious = array();
+            $sectionprevious = array();
+
+            $modinfo = get_fast_modinfo($course);
+            $lastcmid = 0;
+            foreach ($modinfo->cms as $othercm) {
+                if ($othercm->deletioninprogress) {
+                    continue;
+                }
+                // Save first cm of every section.
+                if (!isset($sectionprevious[$othercm->section])) {
+                    $sectionprevious[$othercm->section] = $lastcmid;
+                }
+                // Load previous to all cms with completion.
+                if ($othercm->completion == COMPLETION_TRACKING_NONE) {
+                    continue;
+                }
+                if ($lastcmid) {
+                    $this->modfastprevious[$othercm->id] = $lastcmid;
+                }
+                $lastcmid = $othercm->id;
+            }
+            // Fill empty sections index.
+            $isections = array_reverse($modinfo->get_section_info_all());
+            foreach ($isections as $section) {
+                if (isset($sectionprevious[$section->id])) {
+                    $lastcmid = $sectionprevious[$section->id];
+                } else {
+                    $sectionprevious[$section->id] = $lastcmid;
+                }
+            }
+            $this->sectionfastprevious = $sectionprevious;
+            $previouscache->set("mod_{$course->id}", $this->modfastprevious);
+            $previouscache->set("sec_{$course->id}", $this->sectionfastprevious);
+        }
+    }
+
     /**
      * Returns a more readable keyword corresponding to a completion state.
      *
@@ -136,7 +303,7 @@ class condition extends \core_availability\condition {
      * @param int $completionstate COMPLETION_xx constant
      * @return string Readable keyword
      */
-    protected static function get_lang_string_keyword($completionstate) {
+    protected static function get_lang_string_keyword(int $completionstate): string {
         switch($completionstate) {
             case COMPLETION_INCOMPLETE:
                 return 'incomplete';
@@ -151,38 +318,69 @@ class condition extends \core_availability\condition {
         }
     }
 
-    public function get_description($full, $not, \core_availability\info $info) {
-        // Get name for module.
-        $modinfo = $info->get_modinfo();
-        if (!array_key_exists($this->cmid, $modinfo->cms)) {
-            $modname = get_string('missing', 'availability_completion');
+    /**
+     * Obtains a string describing this restriction (whether or not
+     * it actually applies).
+     *
+     * @param bool $full Set true if this is the 'full information' view
+     * @param bool $not Set true if we are inverting the condition
+     * @param info $info Item we're checking
+     * @return string Information string (for admin) about all restrictions on
+     *   this item
+     */
+    public function get_description($full, $not, info $info): string {
+        global $USER;
+        $str = 'requires_';
+        $course = $info->get_course();
+        list($selfcmid, $selfsectionid) = $this->get_selfids($info);
+        $modname = '';
+        // On ajax duplicate get_fast_modinfo is called before $PAGE->set_context
+        // so we cannot use $PAGE->user_is_editing().
+        $coursecontext = \context_course::instance($course->id);
+        $editing = !empty($USER->editing) && has_capability('moodle/course:manageactivities', $coursecontext);
+        if ($this->cmid == self::OPTION_PREVIOUS && $editing) {
+            // Previous activity name could be inconsistent when editing due to partial page loadings.
+            $str .= 'previous_';
         } else {
-            $modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$this->cmid]->id . '/>';
+            // Get name for module.
+            $cmid = $this->get_cmid($course, $selfcmid, $selfsectionid);
+            $modinfo = $info->get_modinfo();
+            if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) {
+                $modname = get_string('missing', 'availability_completion');
+            } else {
+                $modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$cmid]->id . '/>';
+            }
         }
 
-        // Work out which lang string to use.
+        // Work out which lang string to use depending on required completion status.
         if ($not) {
             // Convert NOT strings to use the equivalent where possible.
             switch ($this->expectedcompletion) {
                 case COMPLETION_INCOMPLETE:
-                    $str = 'requires_' . self::get_lang_string_keyword(COMPLETION_COMPLETE);
+                    $str .= self::get_lang_string_keyword(COMPLETION_COMPLETE);
                     break;
                 case COMPLETION_COMPLETE:
-                    $str = 'requires_' . self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
+                    $str .= self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
                     break;
                 default:
                     // The other two cases do not have direct opposites.
-                    $str = 'requires_not_' . self::get_lang_string_keyword($this->expectedcompletion);
+                    $str .= 'not_' . self::get_lang_string_keyword($this->expectedcompletion);
                     break;
             }
         } else {
-            $str = 'requires_' . self::get_lang_string_keyword($this->expectedcompletion);
+            $str .= self::get_lang_string_keyword($this->expectedcompletion);
         }
 
         return get_string($str, 'availability_completion', $modname);
     }
 
-    protected function get_debug_string() {
+    /**
+     * Obtains a representation of the options of this condition as a string,
+     * for debugging.
+     *
+     * @return string Text representation of parameters
+     */
+    protected function get_debug_string(): string {
         switch ($this->expectedcompletion) {
             case COMPLETION_COMPLETE :
                 $type = 'COMPLETE';
@@ -199,18 +397,38 @@ class condition extends \core_availability\condition {
             default:
                 throw new \coding_exception('Unexpected expected completion');
         }
-        return 'cm' . $this->cmid . ' ' . $type;
+        $cm = $this->cmid;
+        if ($this->cmid == self::OPTION_PREVIOUS) {
+            $cm = 'opprevious';
+        }
+        return 'cm' . $cm . ' ' . $type;
     }
 
-    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
+    /**
+     * Updates this node after restore, returning true if anything changed.
+     *
+     * @see \core_availability\tree_node\update_after_restore
+     *
+     * @param string $restoreid Restore ID
+     * @param int $courseid ID of target course
+     * @param \base_logger $logger Logger for any warnings
+     * @param string $name Name of this item (for use in warning messages)
+     * @return bool True if there was any change
+     */
+    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name): bool {
         global $DB;
+        $res = false;
+        // If we depend on the previous activity, no translation is needed.
+        if ($this->cmid == self::OPTION_PREVIOUS) {
+            return $res;
+        }
         $rec = \restore_dbops::get_backup_ids_record($restoreid, 'course_module', $this->cmid);
         if (!$rec || !$rec->newitemid) {
             // If we are on the same course (e.g. duplicate) then we can just
             // use the existing one.
             if ($DB->record_exists('course_modules',
                     array('id' => $this->cmid, 'course' => $courseid))) {
-                return false;
+                return $res;
             }
             // Otherwise it's a warning.
             $this->cmid = 0;
@@ -231,7 +449,7 @@ class condition extends \core_availability\condition {
      * @param int $cmid Course-module id
      * @return bool True if this is used in a condition, false otherwise
      */
-    public static function completion_value_used($course, $cmid) {
+    public static function completion_value_used($course, $cmid): bool {
         // Have we already worked out a list of required completion values
         // for this course? If so just use that.
         if (!array_key_exists($course->id, self::$modsusedincondition)) {
@@ -247,7 +465,10 @@ class condition extends \core_availability\condition {
                 $ci = new \core_availability\info_module($othercm);
                 $tree = $ci->get_availability_tree();
                 foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
-                    self::$modsusedincondition[$course->id][$cond->cmid] = true;
+                    $condcmid = $cond->get_cmid($course, $othercm->id, null);
+                    if (!empty($condcmid)) {
+                        self::$modsusedincondition[$course->id][$condcmid] = true;
+                    }
                 }
             }
 
@@ -259,7 +480,10 @@ class condition extends \core_availability\condition {
                 $ci = new \core_availability\info_section($section);
                 $tree = $ci->get_availability_tree();
                 foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
-                    self::$modsusedincondition[$course->id][$cond->cmid] = true;
+                    $condcmid = $cond->get_cmid($course, null, $section->id);
+                    if (!empty($condcmid)) {
+                        self::$modsusedincondition[$course->id][$condcmid] = true;
+                    }
                 }
             }
         }
index 7427328..4659717 100644 (file)
@@ -61,6 +61,7 @@ class frontend extends \core_availability\frontend {
             $context = \context_course::instance($course->id);
             $cms = array();
             $modinfo = get_fast_modinfo($course);
+            $previouscm = false;
             foreach ($modinfo->cms as $id => $othercm) {
                 // Add each course-module if it has completion turned on and is not
                 // the one currently being edited.
@@ -69,8 +70,16 @@ class frontend extends \core_availability\frontend {
                         'name' => format_string($othercm->name, true, array('context' => $context)),
                         'completiongradeitemnumber' => $othercm->completiongradeitemnumber);
                 }
+                if (count($cms) && (empty($cm) || $cm->id == $id)) {
+                    $previouscm = true;
+                }
+            }
+            if ($previouscm) {
+                $previous = (object)array('id' => \availability_completion\condition::OPTION_PREVIOUS,
+                        'name' => get_string('option_previous', 'availability_completion'),
+                        'completiongradeitemnumber' => \availability_completion\condition::OPTION_PREVIOUS);
+                array_unshift($cms, $previous);
             }
-
             $this->cachekey = $cachekey;
             $this->cacheinitparams = array($cms);
         }
diff --git a/availability/condition/completion/db/caches.php b/availability/condition/completion/db/caches.php
new file mode 100644 (file)
index 0000000..e365533
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Defined caches used internally by the plugin.
+ *
+ * @package     availability_completion
+ * @category    cache
+ * @copyright   2020 Ferran Recio <ferran@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$definitions = [
+    'previous_cache' => [
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true
+    ],
+];
index f3a8693..4d406da 100644 (file)
@@ -22,6 +22,7 @@
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['cachedef_previous_cache'] = 'Previous activity dependency information';
 $string['description'] = 'Require students to complete (or not complete) another activity.';
 $string['error_selectcmid'] = 'You must select an activity for the completion condition.';
 $string['error_selectcmidpassfail'] = 'You must select an activity with "Require grade" completion condition set.';
@@ -32,6 +33,7 @@ $string['option_complete'] = 'must be marked complete';
 $string['option_fail'] = 'must be complete with fail grade';
 $string['option_incomplete'] = 'must not be marked complete';
 $string['option_pass'] = 'must be complete with pass grade';
+$string['option_previous'] = 'Previous activity with completion';
 $string['pluginname'] = 'Restriction by activity completion';
 $string['requires_incomplete'] = 'The activity <strong>{$a}</strong> is incomplete';
 $string['requires_complete'] = 'The activity <strong>{$a}</strong> is marked complete';
@@ -39,5 +41,11 @@ $string['requires_complete_pass'] = 'The activity <strong>{$a}</strong> is compl
 $string['requires_complete_fail'] = 'The activity <strong>{$a}</strong> is complete and failed';
 $string['requires_not_complete_pass'] = 'The activity <strong>{$a}</strong> is not complete and passed';
 $string['requires_not_complete_fail'] = 'The activity <strong>{$a}</strong> is not complete and failed';
+$string['requires_previous_incomplete'] = 'The <strong>previous activity with completion</strong> is incomplete';
+$string['requires_previous_complete'] = 'The <strong>previous activity with completion</strong> is marked complete';
+$string['requires_previous_complete_pass'] = 'The <strong>previous activity with completion</strong> is complete and passed';
+$string['requires_previous_complete_fail'] = 'The <strong>previous activity with completion</strong> is complete and failed';
+$string['requires_previous_not_complete_pass'] = 'The <strong>previous activity with completion</strong> is not complete and passed';
+$string['requires_previous_not_complete_fail'] = 'The <strong>previous activity with completion</strong> is not complete and failed';
 $string['title'] = 'Activity completion';
 $string['privacy:metadata'] = 'The Restriction by activity completion plugin does not store any personal data.';
diff --git a/availability/condition/completion/tests/behat/availability_completion_previous.feature b/availability/condition/completion/tests/behat/availability_completion_previous.feature
new file mode 100644 (file)
index 0000000..ae21266
--- /dev/null
@@ -0,0 +1,200 @@
+@availability @availability_completion
+Feature: Confirm that availability_completion works with previous activity setting
+  In order to control student access to activities
+  As a teacher
+  I need to set completion conditions which prevent student access
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format | enablecompletion | numsections |
+      | Course 1 | C1        | topics | 1                | 5           |
+    And the following "users" exist:
+      | username |
+      | teacher1 |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    Given the following "activities" exist:
+      | activity | name           | intro              | course | idnumber | groupmode | completion | section |
+      | page     | Page1          | Page 1 description | C1     | page1    | 1         | 1          | 1       |
+      | page     | Page Ignored 1 | Page Ignored       | C1     | pagei1   | 1         | 0          | 1       |
+      | page     | Page2          | Page 2 description | C1     | page2    | 1         | 1          | 3       |
+      | page     | Page3          | Page 3 description | C1     | page3    | 1         | 1          | 4       |
+
+  @javascript
+  Scenario: Test condition with previous activity on an activity
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set Page3 restriction to Previous Activity with completion.
+    When I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Remove Page 2 and check Page3 depends now on Page1.
+    When I turn editing mode on
+    And I delete "Page2" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test previous activity availability when duplicate an activity
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set Page3 restriction to Previous Activity with completion.
+    When I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Duplicate Page3.
+    When I turn editing mode on
+    And I duplicate "Page3" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test previous activity availability when modify completion tacking
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set Page3 restriction to Previous Activity with completion.
+    When I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Test if I disable completion tracking on Page2 section 5 depends on Page2.
+    When I turn editing mode on
+    When I open "Page2" actions menu
+    And I click on "Edit settings" "link" in the "Page2" activity
+    And I set the following fields to these values:
+      | Completion tracking | Do not indicate activity completion |
+    And I press "Save and return to course"
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test condition with previous activity on a section
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set section 4 restriction to Previous Activity with completion.
+    When I edit the section "4"
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save changes"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+    # Remove Page 2 and check Section 4 depends now on Page1.
+    When I turn editing mode on
+    And I delete "Page2" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test condition with previous activity on the first activity of the course
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Try to set Page1 restriction to Previous Activity with completion.
+    When I open "Page1" actions menu
+    And I click on "Edit settings" "link" in the "Page1" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    Then the "Activity or resource" select box should not contain "Previous activity with completion"
+
+    # Set Page2 restriction to Previous Activity with completion and delete Page1.
+    When I am on "Course 1" course homepage
+    When I open "Page2" actions menu
+    And I click on "Edit settings" "link" in the "Page2" activity
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save and return to course"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    # Delete Page 1 and check than Page2 now depends on a missing activity (no previous activity found).
+    When I am on "Course 1" course homepage
+    And I delete "Page1" activity
+    And I turn editing mode off
+    Then I should see "Not available unless: The activity (Missing activity)" in the "region-main" "region"
+
+  @javascript
+  Scenario: Test previous activities on empty sections
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+
+    # Set section 2 restriction to Previous Activity with completion.
+    When I edit the section "2"
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save changes"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+    # Set section 5 restriction to Previous Activity with completion.
+    When I turn editing mode on
+    And I edit the section "5"
+    And I expand all fieldsets
+    And I click on "Add restriction..." "button"
+    And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+    And I click on "Displayed greyed-out if user does not meet this condition â€¢ Click to hide" "link"
+    And I set the field "Activity or resource" to "Previous activity with completion"
+    And I press "Save changes"
+    Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region"
+
+    # Test if I disable completion tracking on Page3 section 5 depends on Page2.
+    When I turn editing mode on
+    And I open "Page3" actions menu
+    And I click on "Edit settings" "link" in the "Page3" activity
+    And I set the following fields to these values:
+      | Completion tracking | Do not indicate activity completion |
+    And I press "Save and return to course"
+
+    When I turn editing mode off
+    Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
index c64be08..bb60039 100644 (file)
@@ -37,13 +37,23 @@ require_once($CFG->libdir . '/completionlib.php');
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class availability_completion_condition_testcase extends advanced_testcase {
+
     /**
-     * Load required classes.
+     * Setup to ensure that fixtures are loaded.
      */
-    public function setUp() {
-        // Load the mock info class so that it can be used.
+    public static function setupBeforeClass(): void {
         global $CFG;
+        // Load the mock info class so that it can be used.
         require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
+        require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_module.php');
+        require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_section.php');
+    }
+
+    /**
+     * Load required classes.
+     */
+    public function setUp() {
+        availability_completion\condition::wipe_static_cache();
     }
 
     /**
@@ -62,6 +72,8 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $course = $generator->create_course(array('enablecompletion' => 1));
         $page = $generator->get_plugin_generator('mod_page')->create_instance(
                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+        $selfpage = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
 
         $modinfo = get_fast_modinfo($course);
         $cm = $modinfo->get_cm($page->cmid);
@@ -142,6 +154,12 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $structure->e = COMPLETION_INCOMPLETE;
         $cond = new condition($structure);
         $this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond);
+
+        // Successful contruct with previous activity.
+        $structure->cm = condition::OPTION_PREVIOUS;
+        $cond = new condition($structure);
+        $this->assertEquals('{completion:cmopprevious INCOMPLETE}', (string)$cond);
+
     }
 
     /**
@@ -339,12 +357,318 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
     }
 
+    /**
+     * Tests the is_available and get_description functions for previous activity option.
+     *
+     * @dataProvider test_previous_activity_data
+     * @param int $grade the current assign grade (0 for none)
+     * @param int $condition true for complete, false for incomplete
+     * @param string $mark activity to mark as complete
+     * @param string $activity activity name to test
+     * @param bool $result if it must be available or not
+     * @param bool $resultnot if it must be available when the condition is inverted
+     * @param string $description the availabiklity text to check
+     */
+    public function test_previous_activity(int $grade, int $condition, string $mark, string $activity,
+            bool $result, bool $resultnot, string $description): void {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/assign/locallib.php');
+        $this->resetAfterTest();
+
+        // Create course with completion turned on.
+        $CFG->enablecompletion = true;
+        $CFG->enableavailability = true;
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(array('enablecompletion' => 1));
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $this->setUser($user);
+
+        // Page 1 (manual completion).
+        $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'name' => 'Page1!',
+                'completion' => COMPLETION_TRACKING_MANUAL));
+
+        // Page 2 (manual completion).
+        $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'name' => 'Page2!',
+                'completion' => COMPLETION_TRACKING_MANUAL));
+
+        // Page ignored (no completion).
+        $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'name' => 'Page ignored!'));
+
+        // Create an assignment - we need to have something that can be graded
+        // so as to test the PASS/FAIL states. Set it up to be completed based
+        // on its grade item.
+        $assignrow = $this->getDataGenerator()->create_module('assign', array(
+                'course' => $course->id, 'name' => 'Assign!',
+                'completion' => COMPLETION_TRACKING_AUTOMATIC));
+        $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
+                array('id' => $assignrow->cmid));
+        $assign = new assign(context_module::instance($assignrow->cmid), false, false);
+
+        // Page 3 (manual completion).
+        $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'name' => 'Page3!',
+                'completion' => COMPLETION_TRACKING_MANUAL));
+
+        // Get basic details.
+        $activities = [];
+        $modinfo = get_fast_modinfo($course);
+        $activities['page1'] = $modinfo->get_cm($page1->cmid);
+        $activities['page2'] = $modinfo->get_cm($page2->cmid);
+        $activities['assign'] = $assign->get_course_module();
+        $activities['page3'] = $modinfo->get_cm($page3->cmid);
+        $prevvalue = condition::OPTION_PREVIOUS;
+
+        // Setup gradings and completion.
+        if ($grade) {
+            $gradeitem = $assign->get_grade_item();
+            grade_object::set_properties($gradeitem, array('gradepass' => 50.0));
+            $gradeitem->update();
+            self::set_grade($assignrow, $user->id, $grade);
+        }
+        if ($mark) {
+            $completion = new completion_info($course);
+            $completion->update_state($activities[$mark], COMPLETION_COMPLETE);
+        }
+
+        // Set opprevious WITH non existent previous activity.
+        $info = new \core_availability\mock_info_module($user->id, $activities[$activity]);
+        $cond = new condition((object)array(
+                'cm' => (int)$prevvalue, 'e' => $condition));
+
+        // Do the checks.
+        $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
+        $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
+        $information = $cond->get_description(false, false, $info);
+        $information = \core_availability\info::format_info($information, $course);
+        $this->assertRegExp($description, $information);
+    }
+
+    public function test_previous_activity_data(): array {
+        // Assign grade, condition, activity to complete, activity to test, result, resultnot, description.
+        return [
+            'Missing previous activity complete' => [
+                0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~'
+            ],
+            'Missing previous activity incomplete' => [
+                0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~'
+            ],
+            'Previous complete condition with previous activity incompleted' => [
+                0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous activity incompleted' => [
+                0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~'
+            ],
+            'Previous complete condition with previous activity completed' => [
+                0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous activity completed' => [
+                0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~'
+            ],
+            // Depenging on page pass fail (pages are not gradable).
+            'Previous complete pass condition with previous no gradable activity incompleted' => [
+                0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous no gradable activity incompleted' => [
+                0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~'
+            ],
+            'Previous complete pass condition with previous no gradable activity completed' => [
+                0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous no gradable activity completed' => [
+                0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~'
+            ],
+            // There's an page without completion between page2 ans assign.
+            'Previous complete condition with sibling activity incompleted' => [
+                0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~'
+            ],
+            'Previous incomplete condition with sibling activity incompleted' => [
+                0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~'
+            ],
+            'Previous complete condition with sibling activity completed' => [
+                0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~'
+            ],
+            'Previous incomplete condition with sibling activity completed' => [
+                0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~'
+            ],
+            // Depending on assign without grade.
+            'Previous complete condition with previous without grade' => [
+                0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous without grade' => [
+                0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~'
+            ],
+            'Previous complete pass condition with previous without grade' => [
+                0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous without grade' => [
+                0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
+            ],
+            // Depending on assign with grade.
+            'Previous complete condition with previous fail grade' => [
+                40, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous fail grade' => [
+                40, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
+            ],
+            'Previous complete pass condition with previous fail grade' => [
+                40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous fail grade' => [
+                40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~'
+            ],
+            'Previous complete condition with previous pass grade' => [
+                60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
+            ],
+            'Previous incomplete condition with previous pass grade' => [
+                60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
+            ],
+            'Previous complete pass condition with previous pass grade' => [
+                60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~'
+            ],
+            'Previous complete fail condition with previous pass grade' => [
+                60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
+            ],
+        ];
+    }
+
+    /**
+     * Tests the is_available and get_description functions for
+     * previous activity option in course sections.
+     *
+     * @dataProvider test_section_previous_activity_data
+     * @param int $condition condition value
+     * @param bool $mark if Page 1 must be mark as completed
+     * @param string $section section to add the availability
+     * @param bool $result expected result
+     * @param bool $resultnot expected negated result
+     * @param string $description description to match
+     */
+    public function test_section_previous_activity(int $condition, bool $mark, string $section,
+                bool $result, bool $resultnot, string $description): void {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/mod/assign/locallib.php');
+        $this->resetAfterTest();
+
+        // Create course with completion turned on.
+        $CFG->enablecompletion = true;
+        $CFG->enableavailability = true;
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(
+                array('numsections' => 4, 'enablecompletion' => 1),
+                array('createsections' => true));
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $this->setUser($user);
+
+        // Section 1 - page1 (manual completion).
+        $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'name' => 'Page1!', 'section' => 1,
+                'completion' => COMPLETION_TRACKING_MANUAL));
+
+        // Section 1 - page ignored 1 (no completion).
+        $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course, 'name' => 'Page ignored!', 'section' => 1));
+
+        // Section 2 - page ignored 2 (no completion).
+        $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course, 'name' => 'Page ignored!', 'section' => 2));
+
+        // Section 3 - page2 (manual completion).
+        $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'name' => 'Page2!', 'section' => 3,
+                'completion' => COMPLETION_TRACKING_MANUAL));
+
+        // Section 4 is empty.
+
+        // Get basic details.
+        get_fast_modinfo(0, 0, true);
+        $modinfo = get_fast_modinfo($course);
+        $sections['section1'] = $modinfo->get_section_info(1);
+        $sections['section2'] = $modinfo->get_section_info(2);
+        $sections['section3'] = $modinfo->get_section_info(3);
+        $sections['section4'] = $modinfo->get_section_info(4);
+        $page1cm = $modinfo->get_cm($page1->cmid);
+        $prevvalue = condition::OPTION_PREVIOUS;
+
+        if ($mark) {
+            // Mark page1 complete.
+            $completion = new completion_info($course);
+            $completion->update_state($page1cm, COMPLETION_COMPLETE);
+        }
+
+        $info = new \core_availability\mock_info_section($user->id, $sections[$section]);
+        $cond = new condition((object)array(
+                'cm' => (int)$prevvalue, 'e' => $condition));
+        $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
+        $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
+        $information = $cond->get_description(false, false, $info);
+        $information = \core_availability\info::format_info($information, $course);
+        $this->assertRegExp($description, $information);
+
+    }
+
+    public function test_section_previous_activity_data(): array {
+        return [
+            // Condition, Activity completion, section to test, result, resultnot, description.
+            'Completion complete Section with no previous activity' => [
+                COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~'
+            ],
+            'Completion incomplete Section with no previous activity' => [
+                COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~'
+            ],
+            // Section 2 depending on section 1 -> Page 1 (no grading).
+            'Completion complete Section with previous activity incompleted' => [
+                COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section with previous activity incompleted' => [
+                COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~'
+            ],
+            'Completion complete Section with previous activity completed' => [
+                COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section with previous activity completed' => [
+                COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~'
+            ],
+            // Section 3 depending on section 1 -> Page 1 (no grading).
+            'Completion complete Section ignoring empty sections and activity incompleted' => [
+                COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section ignoring empty sections and activity incompleted' => [
+                COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~'
+            ],
+            'Completion complete Section ignoring empty sections and activity completed' => [
+                COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~'
+            ],
+            'Completion incomplete Section ignoring empty sections and activity completed' => [
+                COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~'
+            ],
+            // Section 4 depending on section 3 -> Page 2 (no grading).
+            'Completion complete Last section with previous activity incompleted' => [
+                COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~'
+            ],
+            'Completion incomplete Last section with previous activity incompleted' => [
+                COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~'
+            ],
+            'Completion complete Last section with previous activity completed' => [
+                COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~'
+            ],
+            'Completion incomplete Last section with previous activity completed' => [
+                COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~'
+            ],
+        ];
+    }
+
     /**
      * Tests completion_value_used static function.
      */
     public function test_completion_value_used() {
         global $CFG, $DB;
         $this->resetAfterTest();
+        $prevvalue = condition::OPTION_PREVIOUS;
 
         // Create course with completion turned on and some sections.
         $CFG->enablecompletion = true;
@@ -353,15 +677,20 @@ class availability_completion_condition_testcase extends advanced_testcase {
         $course = $generator->create_course(
                 array('numsections' => 1, 'enablecompletion' => 1),
                 array('createsections' => true));
-        availability_completion\condition::wipe_static_cache();
 
-        // Create three pages with manual completion.
+        // Create six pages with manual completion.
         $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
         $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
         $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+        $page4 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+        $page5 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+        $page6 = $generator->get_plugin_generator('mod_page')->create_instance(
+                array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
 
         // Set up page3 to depend on page1, and section1 to depend on page2.
         $DB->set_field('course_modules', 'availability',
@@ -372,14 +701,29 @@ class availability_completion_condition_testcase extends advanced_testcase {
                 '{"op":"|","show":true,"c":[' .
                 '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
                 array('course' => $course->id, 'section' => 1));
+        // Set up page5 and page6 to depend on previous activity.
+        $DB->set_field('course_modules', 'availability',
+                '{"op":"|","show":true,"c":[' .
+                '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
+                array('id' => $page5->cmid));
+        $DB->set_field('course_modules', 'availability',
+                '{"op":"|","show":true,"c":[' .
+                '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
+                array('id' => $page6->cmid));
 
-        // Now check: nothing depends on page3 but something does on the others.
+        // Check 1: nothing depends on page3 and page6 but something does on the others.
         $this->assertTrue(availability_completion\condition::completion_value_used(
                 $course, $page1->cmid));
         $this->assertTrue(availability_completion\condition::completion_value_used(
                 $course, $page2->cmid));
         $this->assertFalse(availability_completion\condition::completion_value_used(
                 $course, $page3->cmid));
+        $this->assertTrue(availability_completion\condition::completion_value_used(
+                $course, $page4->cmid));
+        $this->assertTrue(availability_completion\condition::completion_value_used(
+                $course, $page5->cmid));
+        $this->assertFalse(availability_completion\condition::completion_value_used(
+                $course, $page6->cmid));
     }
 
     /**
@@ -402,11 +746,28 @@ class availability_completion_condition_testcase extends advanced_testcase {
      */
     public function test_update_dependency_id() {
         $cond = new condition((object)array(
-                'cm' => 123, 'e' => COMPLETION_COMPLETE));
-        $this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
+                'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43));
+        $this->assertFalse($cond->update_dependency_id('frogs', 42, 540));
         $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
-        $this->assertTrue($cond->update_dependency_id('course_modules', 123, 456));
+        $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456));
         $after = $cond->save();
         $this->assertEquals(456, $after->cm);
+
+        // Test selfid updating.
+        $cond = new condition((object)array(
+                'cm' => 42, 'e' => COMPLETION_COMPLETE));
+        $this->assertFalse($cond->update_dependency_id('frogs', 43, 540));
+        $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
+        $after = $cond->save();
+        $this->assertEquals(42, $after->cm);
+
+        // Test on previous activity.
+        $cond = new condition((object)array(
+                'cm' => condition::OPTION_PREVIOUS,
+                'e' => COMPLETION_COMPLETE));
+        $this->assertFalse($cond->update_dependency_id('frogs', 43, 80));
+        $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
+        $after = $cond->save();
+        $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm);
     }
 }
index 71d6281..4e1a55f 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2019111800;
+$plugin->version = 2020032600;
 $plugin->requires = 2019111200;
 $plugin->component = 'availability_completion';
diff --git a/availability/tests/fixtures/mock_info_module.php b/availability/tests/fixtures/mock_info_module.php
new file mode 100644 (file)
index 0000000..47f75cc
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * For use in unit tests that require an info module which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * For use in unit tests that require an info module which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_info_module extends info_module {
+    /** @var int User id for modinfo */
+    protected $userid;
+
+    /** @var \cm_info Activity. */
+    protected $cm;
+
+    /**
+     * Constructs with item details.
+     *
+     * @param int $userid Userid for modinfo (if used)
+     * @param \cm_info $cm Course-module object
+     */
+    public function __construct($userid = 0, \cm_info $cm = null) {
+        parent::__construct($cm);
+        $this->userid = $userid;
+        $this->cm = $cm;
+    }
+
+    /**
+     * Just returns a mock name.
+     *
+     * @return string Name of item
+     */
+    protected function get_thing_name() {
+        return 'Mock Module';
+    }
+
+    /**
+     * Returns the current context.
+     *
+     * @return \context Context for this item
+     */
+    public function get_context() {
+        return \context_course::instance($this->get_course()->id);
+    }
+
+    /**
+     * Returns the cappability used to ignore access restrictions.
+     *
+     * @return string Name of capability used to view hidden items of this type
+     */
+    protected function get_view_hidden_capability() {
+        return 'moodle/course:ignoreavailabilityrestrictions';
+    }
+
+    /**
+     * Mocks don't need to save anything into DB.
+     *
+     * @param string $availability New JSON value
+     */
+    protected function set_in_database($availability) {
+    }
+
+    /**
+     * Obtains the modinfo associated with this availability information.
+     *
+     * Note: This field is available ONLY for use by conditions when calculating
+     * availability or information.
+     *
+     * @return \course_modinfo Modinfo
+     * @throws \coding_exception If called at incorrect times
+     */
+    public function get_modinfo() {
+        // Allow modinfo usage outside is_available etc., so we can use this
+        // to directly call into condition is_available.
+        if (!$this->userid) {
+            throw new \coding_exception('Need to set mock_info userid');
+        }
+        return get_fast_modinfo($this->course, $this->userid);
+    }
+
+    /**
+     * Override course-module info.
+     * @param \cm_info $cm
+     */
+    public function set_cm (\cm_info $cm) {
+        $this->cm = $cm;
+    }
+}
diff --git a/availability/tests/fixtures/mock_info_section.php b/availability/tests/fixtures/mock_info_section.php
new file mode 100644 (file)
index 0000000..15fba45
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * For use in unit tests that require an info section which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * For use in unit tests that require an info section which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_info_section extends info_section {
+    /** @var int User id for modinfo */
+    protected $userid;
+
+    /** @var \section_info Section. */
+    protected $section;
+
+    /**
+     * Constructs with item details.
+     *
+     * @param int $userid Userid for modinfo (if used)
+     * @param \section_info $section Section object
+     */
+    public function __construct($userid = 0, \section_info $section = null) {
+        parent::__construct($section);
+        $this->userid = $userid;
+        $this->section = $section;
+    }
+
+    /**
+     * Just returns a mock name.
+     *
+     * @return string Name of item
+     */
+    protected function get_thing_name() {
+        return 'Mock Section';
+    }
+
+    /**
+     * Returns the current context.
+     *
+     * @return \context Context for this item
+     */
+    public function get_context() {
+        return \context_course::instance($this->get_course()->id);
+    }
+
+    /**
+     * Returns the cappability used to ignore access restrictions.
+     *
+     * @return string Name of capability used to view hidden items of this type
+     */
+    protected function get_view_hidden_capability() {
+        return 'moodle/course:ignoreavailabilityrestrictions';
+    }
+
+    /**
+     * Mocks don't need to save anything into DB.
+     *
+     * @param string $availability New JSON value
+     */
+    protected function set_in_database($availability) {
+    }
+
+    /**
+     * Obtains the modinfo associated with this availability information.
+     *
+     * Note: This field is available ONLY for use by conditions when calculating
+     * availability or information.
+     *
+     * @return \course_modinfo Modinfo
+     * @throws \coding_exception If called at incorrect times
+     */
+    public function get_modinfo() {
+        // Allow modinfo usage outside is_available etc., so we can use this
+        // to directly call into condition is_available.
+        if (!$this->userid) {
+            throw new \coding_exception('Need to set mock_info userid');
+        }
+        return get_fast_modinfo($this->course, $this->userid);
+    }
+
+    /**
+     * Override section info.
+     *
+     * @param \section_info $section
+     */
+    public function set_section (\section_info $section) {
+        $this->section = $section;
+    }
+}