MDL-35089 conditionals: detected one place missing sectioncache
[moodle.git] / lib / conditionlib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Used for tracking conditions that apply before activities are displayed
19  * to students ('conditional availability').
20  *
21  * @package    core_condition
22  * @category   condition
23  * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * CONDITION_STUDENTVIEW_HIDE - The activity is not displayed to students at all when conditions aren't met.
31  */
32 define('CONDITION_STUDENTVIEW_HIDE', 0);
33 /**
34  * CONDITION_STUDENTVIEW_SHOW - The activity is displayed to students as a greyed-out name, with
35  * informational text that explains the conditions under which it will be available.
36  */
37 define('CONDITION_STUDENTVIEW_SHOW', 1);
39 /**
40  * CONDITION_MISSING_NOTHING - The $item variable is expected to contain all completion-related data
41  */
42 define('CONDITION_MISSING_NOTHING', 0);
43 /**
44  * CONDITION_MISSING_EXTRATABLE - The $item variable is expected to contain the fields from
45  * the relevant table (course_modules or course_sections) but not the _availability data
46  */
47 define('CONDITION_MISSING_EXTRATABLE', 1);
48 /**
49  * CONDITION_MISSING_EVERYTHING - The $item variable is expected to contain nothing except the ID
50  */
51 define('CONDITION_MISSING_EVERYTHING', 2);
53 require_once($CFG->libdir.'/completionlib.php');
55 /**
56  * Core class to handle conditional activites
57  *
58  * @package   core_condition
59  * @category  condition
60  * @copyright 2012 The Open University
61  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62  */
63 class condition_info extends condition_info_base {
64     /**
65      * Constructs with course-module details.
66      *
67      * @global moodle_database $DB
68      * @uses CONDITION_MISSING_NOTHING
69      * @param object $cm Moodle course-module object. May have extra fields
70      *   ->conditionsgrade, ->conditionscompletion which should come from
71      *   get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
72      *   and ->showavailability, ->course, ->visible; but the only required
73      *   thing is ->id.
74      * @param int $expectingmissing Used to control whether or not a developer
75      *   debugging message (performance warning) will be displayed if some of
76      *   the above data is missing and needs to be retrieved; a
77      *   CONDITION_MISSING_xx constant
78      * @param bool $loaddata If you need a 'write-only' object, set this value
79      *   to false to prevent database access from constructor
80      */
81     public function __construct($cm, $expectingmissing = CONDITION_MISSING_NOTHING,
82         $loaddata=true) {
83         parent::__construct($cm, 'course_modules', 'coursemoduleid',
84                 $expectingmissing, $loaddata);
85     }
87     /**
88      * Adds the extra availability conditions (if any) into the given
89      * course-module (or section) object.
90      *
91      * This function may be called statically (for the editing form) or
92      * dynamically.
93      *
94      * @param object $cm Moodle course-module data object
95      */
96     public static function fill_availability_conditions($cm) {
97         parent::fill_availability_conditions_inner($cm, 'course_modules', 'coursemoduleid');
98     }
100     /**
101      * Gets the course-module object with full necessary data to determine availability.
102      * @return object Course-module object with full data
103      * @throws coding_exception If data was not supplied when constructing object
104      */
105     public function get_full_course_module() {
106         return $this->get_full_item();
107     }
109     /**
110      * Utility function called by modedit.php; updates the
111      * course_modules_availability table based on the module form data.
112      *
113      * @param object $cm Course-module with as much data as necessary, min id
114      * @param object $fromform Data from form
115      * @param bool $wipefirst If true, wipes existing conditions
116      */
117     public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
118         $ci = new condition_info($cm, CONDITION_MISSING_EVERYTHING, false);
119         parent::update_from_form($ci, $fromform, $wipefirst);
120     }
122     /**
123      * Used in course/lib.php because we need to disable the completion JS if
124      * a completion value affects a conditional activity.
125      *
126      * @global stdClass $CONDITIONLIB_PRIVATE
127      * @param object $course Moodle course object
128      * @param object $item Moodle course-module
129      * @return bool True if this is used in a condition, false otherwise
130      */
131     public static function completion_value_used_as_condition($course, $cm) {
132         // Have we already worked out a list of required completion values
133         // for this course? If so just use that
134         global $CONDITIONLIB_PRIVATE, $DB;
135         if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) {
136             // We don't have data for this course, build it
137             $modinfo = get_fast_modinfo($course);
138             $CONDITIONLIB_PRIVATE->usedincondition[$course->id] = array();
140             // Activities
141             foreach ($modinfo->cms as $othercm) {
142                 foreach ($othercm->conditionscompletion as $cmid => $expectedcompletion) {
143                     $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
144                 }
145             }
147             // Sections
148             foreach ($modinfo->get_section_info_all() as $section) {
149                 foreach ($section->conditionscompletion as $cmid => $expectedcompletion) {
150                     $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
151                 }
152             }
153         }
154         return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]);
155     }
159 /**
160  * Handles conditional access to sections.
161  *
162  * @package   core_condition
163  * @category  condition
164  * @copyright 2012 The Open University
165  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
166  */
167 class condition_info_section extends condition_info_base {
168     /**
169      * Constructs with course-module details.
170      *
171      * @global moodle_database $DB
172      * @uses CONDITION_MISSING_NOTHING
173      * @param object $section Moodle section object. May have extra fields
174      *   ->conditionsgrade, ->conditionscompletion. Should have ->availablefrom,
175      *   ->availableuntil, and ->showavailability, ->course; but the only
176      *   required thing is ->id.
177      * @param int $expectingmissing Used to control whether or not a developer
178      *   debugging message (performance warning) will be displayed if some of
179      *   the above data is missing and needs to be retrieved; a
180      *   CONDITION_MISSING_xx constant
181      * @param bool $loaddata If you need a 'write-only' object, set this value
182      *   to false to prevent database access from constructor
183      */
184     public function __construct($section, $expectingmissing = CONDITION_MISSING_NOTHING,
185         $loaddata=true) {
186         parent::__construct($section, 'course_sections', 'coursesectionid',
187                 $expectingmissing, $loaddata);
188     }
190     /**
191      * Adds the extra availability conditions (if any) into the given
192      * course-module (or section) object.
193      *
194      * This function may be called statically (for the editing form) or
195      * dynamically.
196      *
197      * @param object $section Moodle section data object
198      */
199     public static function fill_availability_conditions($section) {
200         parent::fill_availability_conditions_inner($section, 'course_sections', 'coursesectionid');
201     }
203     /**
204      * Gets the section object with full necessary data to determine availability.
205      * @return object Section object with full data
206      * @throws coding_exception If data was not supplied when constructing object
207      */
208     public function get_full_section() {
209         return $this->get_full_item();
210     }
212     /**
213      * Gets list of required fields from main table.
214      * @return array Array of field names
215      */
216     protected function get_main_table_fields() {
217         return array_merge(parent::get_main_table_fields(), array('groupingid'));
218     }
220     /**
221      * Determines whether this particular section is currently available
222      * according to these criteria.
223      *
224      * - This does not include the 'visible' setting (i.e. this might return
225      *   true even if visible is false); visible is handled independently.
226      * - This does not take account of the viewhiddenactivities capability.
227      *   That should apply later.
228      *
229      * @global moodle_database $DB
230      * @global stdclass $USER
231      * @param string $information If the item has availability restrictions,
232      *   a string that describes the conditions will be stored in this variable;
233      *   if this variable is set blank, that means don't display anything
234      * @param bool $grabthelot Performance hint: if true, caches information
235      *   required for all course-modules, to make the front page and similar
236      *   pages work more quickly (works only for current user)
237      * @param int $userid If set, specifies a different user ID to check availability for
238      * @param object $modinfo Usually leave as null for default. Specify when
239      *   calling recursively from inside get_fast_modinfo. The value supplied
240      *   here must include list of all CMs with 'id' and 'name'
241      * @return bool True if this item is available to the user, false otherwise
242      */
243     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
244         global $DB, $USER, $CONDITIONLIB_PRIVATE;
246         $available = parent::is_available($information, $grabthelot, $userid, $modinfo);
248         // test if user is enrolled to a grouping which has access to the section
249         if (!empty($this->item->groupingid)) {
250             // Get real user id
251             if (!$userid) {
252                 $userid = $USER->id;
253             }
254             $context = context_course::instance($this->item->course);
256             if ($userid != $USER->id) {
257                 // We are requesting for a non-current user so check it individually
258                 // (no cache). Do grouping check first, it's probably faster
259                 // than the capability check
260                 $gotit = $DB->record_exists_sql('
261                         SELECT
262                             1
263                         FROM
264                             {groupings} g
265                             JOIN {groupings_groups} gg ON g.id = gg.groupingid
266                             JOIN {groups_members} gm ON gg.groupid = gm.groupid
267                         WHERE
268                             g.id = ? AND gm.userid = ?',
269                         array($this->item->groupingid, $userid));
270                 if (!$gotit && !has_capability('moodle/site:accessallgroups', $context, $userid)) {
271                     $available = false;
272                     $information .= get_string('groupingnoaccess', 'condition');
273                 }
274             } else {
275                 // Request is for current user - use cache
276                 if( !array_key_exists($this->item->course, $CONDITIONLIB_PRIVATE->groupingscache)) {
277                     if (has_capability('moodle/site:accessallgroups', $context)) {
278                         $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = true;
279                     } else {
280                         $groupings = $DB->get_records_sql('
281                                 SELECT
282                                     g.id as gid
283                                 FROM
284                                     {groupings} g
285                                     JOIN {groupings_groups} gg ON g.id = gg.groupingid
286                                     JOIN {groups_members} gm ON gg.groupid = gm.groupid
287                                 WHERE
288                                     g.courseid = ? AND gm.userid = ?',
289                                 array($this->item->course, $userid));
290                         $list = array();
291                         foreach ($groupings as $grouping) {
292                             $list[$grouping->gid] = true;
293                         }
294                         $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = $list;
295                     }
296                 }
298                 $usergroupings = $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course];
299                 if ($usergroupings !== true && !array_key_exists($this->item->groupingid, $usergroupings)) {
300                     $available = false;
301                     $information .= get_string('groupingnoaccess', 'condition');
302                 }
303             }
304         }
306         $information = trim($information);
307         return $available;
308     }
310     /**
311      * Utility function called by modedit.php; updates the
312      * course_modules_availability table based on the module form data.
313      *
314      * @param object $section Section object, must at minimum contain id
315      * @param object $fromform Data from form
316      * @param bool $wipefirst If true, wipes existing conditions
317      */
318     public static function update_section_from_form($section, $fromform, $wipefirst=true) {
319         $ci = new condition_info_section($section, CONDITION_MISSING_EVERYTHING);
320         parent::update_from_form($ci, $fromform, $wipefirst);
321     }
325 /**
326  * Base class to handle conditional items of some kind (currently either
327  * course_modules or sections; they must have a corresponding _availability
328  * table).
329  *
330  * @package   core_condition
331  * @category  condition
332  * @copyright 2012 The Open University
333  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
334  */
335 abstract class condition_info_base {
336     /** @var object, bool, string, string, array */
337      protected $item, $gotdata, $availtable, $idfieldname, $usergroupings;
339     /**
340      * Constructs with item details.
341      *
342      * @global moodle_database $DB
343      * @uses CONDITION_MISSING_NOTHING
344      * @uses CONDITION_MISSING_EVERYTHING
345      * @uses CONDITION_MISSING_EXTRATABLE
346      * @uses DEBUG_DEVELOPER
347      * @param object $item Object representing some kind of item (cm or section).
348      *   May have extra fields ->conditionsgrade, ->conditionscompletion.
349      *   Should have ->availablefrom, ->availableuntil, and ->showavailability,
350      *   ->course; but the only required thing is ->id.
351      * @param string $tableprefix Prefix for table used to store availability
352      *   data, e.g. 'course_modules' if we are going to look at
353      *   course_modules_availability.
354      * @param string $idfield Within this table, name of field used as item id
355      *   (e.g. 'coursemoduleid')
356      * @param int $expectingmissing Used to control whether or not a developer
357      *   debugging message (performance warning) will be displayed if some of
358      *   the above data is missing and needs to be retrieved; a
359      *   CONDITION_MISSING_xx constant
360      * @param bool $loaddata If you need a 'write-only' object, set this value
361      *   to false to prevent database access from constructor
362      * @return condition_info Object which can retrieve information about the
363      *   activity
364      */
365     public function __construct($item, $tableprefix, $idfield, $expectingmissing, $loaddata) {
366         global $DB;
368         // Check ID as otherwise we can't do the other queries
369         if (empty($item->id)) {
370             throw new coding_exception('Invalid parameters; item ID not included');
371         }
373         // DB table to store availability conditions
374         $this->availtable = $tableprefix . '_availability';
376         // Name of module/section ID field in DB
377         $this->idfieldname = $idfield;
379         // If not loading data, don't do anything else
380         if (!$loaddata) {
381             $this->item = (object)array('id' => $item->id);
382             $this->gotdata = false;
383             return;
384         }
386         // Missing basic data from course_modules
387         $basicfields = $this->get_main_table_fields();
388         $missingbasicfields = false;
389         foreach ($basicfields as $field) {
390             if (!isset($item->{$field})) {
391                 $missingbasicfields = true;
392                 break;
393             }
394         }
395         if ($missingbasicfields) {
396             if ($expectingmissing<CONDITION_MISSING_EVERYTHING) {
397                 debugging('Performance warning: condition_info constructor is ' .
398                         'faster if you pass in $item with at least basic fields ' .
399                         'from its table. '.
400                         '[This warning can be disabled, see phpdoc.]',
401                         DEBUG_DEVELOPER);
402             }
403             $item = $DB->get_record($tableprefix, array('id' => $item->id),
404                     implode(',', $basicfields), MUST_EXIST);
405         }
407         $this->item = clone($item);
408         $this->gotdata = true;
410         // Missing extra data
411         if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion)) {
412             if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) {
413                 debugging('Performance warning: condition_info constructor is ' .
414                         'faster if you pass in a $item from get_fast_modinfo or ' .
415                         'the equivalent for sections. ' .
416                         '[This warning can be disabled, see phpdoc.]',
417                         DEBUG_DEVELOPER);
418             }
420             $this->fill_availability_conditions($this->item);
421         }
422     }
424     /**
425      * Gets list of required fields from main table.
426      *
427      * @return array Array of field names
428      */
429     protected function get_main_table_fields() {
430         return array('id', 'course', 'visible',
431                 'availablefrom', 'availableuntil', 'showavailability');
432     }
434     /**
435      * Fills availability conditions into the item object, if they are missing,
436      * otherwise does nothing. Called by subclass fill_availability_conditions.
437      * @param object $item Item object
438      * @param string $tableprefix Prefix of name for _availability table e.g. 'course_modules'
439      * @param string $idfield Name of field that contains id e.g. 'coursemoduleid'
440      * @throws coding_exception If item object doesn't have id field
441      */
442     protected static function fill_availability_conditions_inner($item, $tableprefix, $idfield) {
443         global $DB, $CFG;
444         if (empty($item->id)) {
445             throw new coding_exception('Invalid parameters; item ID not included');
446         }
448         // Does nothing if the variables are already present
449         if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion)) {
450             $item->conditionsgrade = array();
451             $item->conditionscompletion = array();
453             $conditions = $DB->get_records_sql('
454                     SELECT
455                         a.id AS aid, gi.*, a.sourcecmid, a.requiredcompletion, a.gradeitemid,
456                         a.grademin as conditiongrademin, a.grademax as conditiongrademax
457                     FROM
458                         {' . $tableprefix . '_availability} a
459                         LEFT JOIN {grade_items} gi ON gi.id = a.gradeitemid
460                     WHERE ' . $idfield . ' = ?', array($item->id));
461             foreach ($conditions as $condition) {
462                 if (!is_null($condition->sourcecmid)) {
463                     $item->conditionscompletion[$condition->sourcecmid] =
464                         $condition->requiredcompletion;
465                 } else {
466                     $minmax = new stdClass;
467                     $minmax->min = $condition->conditiongrademin;
468                     $minmax->max = $condition->conditiongrademax;
469                     $minmax->name = self::get_grade_name($condition);
470                     $item->conditionsgrade[$condition->gradeitemid] = $minmax;
471                 }
472             }
473         }
474     }
476     /**
477      * Obtains the name of a grade item.
478      *
479      * @global object $CFG
480      * @param object $gradeitemobj Object from get_record on grade_items table,
481      *     (can be empty if you want to just get !missing)
482      * @return string Name of item of !missing if it didn't exist
483      */
484     private static function get_grade_name($gradeitemobj) {
485         global $CFG;
486         if (isset($gradeitemobj->id)) {
487             require_once($CFG->libdir . '/gradelib.php');
488             $item = new grade_item;
489             grade_object::set_properties($item, $gradeitemobj);
490             return $item->get_name();
491         } else {
492             return '!missing'; // Ooops, missing grade
493         }
494     }
496     /**
497      * Gets the item object with full necessary data to determine availability.
498      * @return object Item object with full data
499      * @throws coding_exception If data was not supplied when constructing object
500      */
501     protected function get_full_item() {
502         $this->require_data();
503         return $this->item;
504     }
506     /**
507      * Adds to the database a condition based on completion of another module.
508      *
509      * @global moodle_database $DB
510      * @param int $cmid ID of other module
511      * @param int $requiredcompletion COMPLETION_xx constant
512      */
513     public function add_completion_condition($cmid, $requiredcompletion) {
514         global $DB;
515         // Add to DB
516         $DB->insert_record($this->availtable, (object)array(
517                 $this->idfieldname => $this->item->id,
518                 'sourcecmid' => $cmid, 'requiredcompletion' => $requiredcompletion),
519                 false);
521         // Store in memory too
522         $this->item->conditionscompletion[$cmid] = $requiredcompletion;
523     }
525     /**
526      * Adds to the database a condition based on the value of a grade item.
527      *
528      * @global moodle_database $DB
529      * @param int $gradeitemid ID of grade item
530      * @param float $min Minimum grade (>=), up to 5 decimal points, or null if none
531      * @param float $max Maximum grade (<), up to 5 decimal points, or null if none
532      * @param bool $updateinmemory If true, updates data in memory; otherwise,
533      *   memory version may be out of date (this has performance consequences,
534      *   so don't do it unless it really needs updating)
535      */
536     public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
537         global $DB;
538         // Normalise nulls
539         if ($min==='') {
540             $min = null;
541         }
542         if ($max==='') {
543             $max = null;
544         }
545         // Add to DB
546         $DB->insert_record($this->availtable, (object)array(
547                 $this->idfieldname => $this->item->id,
548                 'gradeitemid' => $gradeitemid, 'grademin' => $min, 'grademax' => $max),
549                 false);
551         // Store in memory too
552         if ($updateinmemory) {
553             $this->item->conditionsgrade[$gradeitemid] = (object) array(
554                     'min' => $min, 'max' => $max);
555             $this->item->conditionsgrade[$gradeitemid]->name = self::get_grade_name(
556                     $DB->get_record('grade_items', array('id'=>$gradeitemid)));
557         }
558     }
560      /**
561      * Erases from the database all conditions for this activity.
562      *
563      * @global moodle_database $DB
564      */
565     public function wipe_conditions() {
566         // Wipe from DB
567         global $DB;
568         $DB->delete_records($this->availtable, array($this->idfieldname => $this->item->id));
570         // And from memory
571         $this->item->conditionsgrade = array();
572         $this->item->conditionscompletion = array();
573     }
575     /**
576      * Obtains a string describing all availability restrictions (even if
577      * they do not apply any more).
578      *
579      * @global stdClass $COURSE
580      * @global moodle_database $DB
581      * @param object $modinfo Usually leave as null for default. Specify when
582      *   calling recursively from inside get_fast_modinfo. The value supplied
583      *   here must include list of all CMs with 'id' and 'name'
584      * @return string Information string (for admin) about all restrictions on
585      *   this item
586      */
587     public function get_full_information($modinfo=null) {
588         global $COURSE, $DB;
589         $this->require_data();
591         $information = '';
593         // Completion conditions
594         if (count($this->item->conditionscompletion) > 0) {
595             if ($this->item->course == $COURSE->id) {
596                 $course = $COURSE;
597             } else {
598                 $course = $DB->get_record('course', array('id' => $this->item->course),
599                         'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
600             }
601             foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
602                 if (!$modinfo) {
603                     $modinfo = get_fast_modinfo($course);
604                 }
605                 if (empty($modinfo->cms[$cmid])) {
606                     continue;
607                 }
608                 $information .= get_string(
609                         'requires_completion_' . $expectedcompletion,
610                         'condition', $modinfo->cms[$cmid]->name) . ' ';
611             }
612         }
614         // Grade conditions
615         if (count($this->item->conditionsgrade) > 0) {
616             foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
617                 // String depends on type of requirement. We are coy about
618                 // the actual numbers, in case grades aren't released to
619                 // students.
620                 if (is_null($minmax->min) && is_null($minmax->max)) {
621                     $string = 'any';
622                 } else if (is_null($minmax->max)) {
623                     $string = 'min';
624                 } else if (is_null($minmax->min)) {
625                     $string = 'max';
626                 } else {
627                     $string = 'range';
628                 }
629                 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
630             }
631         }
633         // The date logic is complicated. The intention of this logic is:
634         // 1) display date without time where possible (whenever the date is
635         //    midnight)
636         // 2) when the 'until' date is e.g. 00:00 on the 14th, we display it as
637         //    'until the 13th' (experience at the OU showed that students are
638         //    likely to interpret 'until <date>' as 'until the end of <date>').
639         // 3) This behaviour becomes confusing for 'same-day' dates where there
640         //    are some exceptions.
641         // Users in different time zones will typically not get the 'abbreviated'
642         // behaviour but it should work OK for them aside from that.
644         // The following cases are possible:
645         // a) From 13:05 on 14 Oct until 12:10 on 17 Oct (exact, exact)
646         // b) From 14 Oct until 12:11 on 17 Oct (midnight, exact)
647         // c) From 13:05 on 14 Oct until 17 Oct (exact, midnight 18 Oct)
648         // d) From 14 Oct until 17 Oct (midnight 14 Oct, midnight 18 Oct)
649         // e) On 14 Oct (midnight 14 Oct, midnight 15 Oct)
650         // f) From 13:05 on 14 Oct until 0:00 on 15 Oct (exact, midnight, same day)
651         // g) From 0:00 on 14 Oct until 12:05 on 14 Oct (midnight, exact, same day)
652         // h) From 13:05 on 14 Oct (exact)
653         // i) From 14 Oct (midnight)
654         // j) Until 13:05 on 14 Oct (exact)
655         // k) Until 14 Oct (midnight 15 Oct)
657         // Check if start and end dates are 'midnights', if so we show in short form
658         $shortfrom = self::is_midnight($this->item->availablefrom);
659         $shortuntil = self::is_midnight($this->item->availableuntil);
661         // For some checks and for display, we need the previous day for the 'until'
662         // value, if we are going to display it in short form
663         if ($this->item->availableuntil) {
664             $daybeforeuntil = strtotime('-1 day', usergetmidnight($this->item->availableuntil));
665         }
667         // Special case for if one but not both are exact and they are within a day
668         if ($this->item->availablefrom && $this->item->availableuntil &&
669                 $shortfrom != $shortuntil && $daybeforeuntil < $this->item->availablefrom) {
670             // Don't use abbreviated version (see examples f, g above)
671             $shortfrom = false;
672             $shortuntil = false;
673         }
675         // When showing short end date, the display time is the 'day before' one
676         $displayuntil = $shortuntil ? $daybeforeuntil : $this->item->availableuntil;
678         if ($this->item->availablefrom && $this->item->availableuntil) {
679             if ($shortfrom && $shortuntil && $daybeforeuntil == $this->item->availablefrom) {
680                 $information .= get_string('requires_date_both_single_day', 'condition',
681                         self::show_time($this->item->availablefrom, true));
682             } else {
683                 $information .= get_string('requires_date_both', 'condition', (object)array(
684                          'from' => self::show_time($this->item->availablefrom, $shortfrom),
685                          'until' => self::show_time($displayuntil, $shortuntil)));
686             }
687         } else if ($this->item->availablefrom) {
688             $information .= get_string('requires_date', 'condition',
689                 self::show_time($this->item->availablefrom, $shortfrom));
690         } else if ($this->item->availableuntil) {
691             $information .= get_string('requires_date_before', 'condition',
692                 self::show_time($displayuntil, $shortuntil));
693         }
695         $information = trim($information);
696         return $information;
697     }
699     /**
700      * Checks whether a given time refers exactly to midnight (in current user
701      * timezone).
702      *
703      * @param int $time Time
704      * @return bool True if time refers to midnight, false if it's some other
705      *   time or if it is set to zero
706      */
707     private static function is_midnight($time) {
708         return $time && usergetmidnight($time) == $time;
709     }
711     /**
712      * Determines whether this particular item is currently available
713      * according to these criteria.
714      *
715      * - This does not include the 'visible' setting (i.e. this might return
716      *   true even if visible is false); visible is handled independently.
717      * - This does not take account of the viewhiddenactivities capability.
718      *   That should apply later.
719      *
720      * @global stdClass $COURSE
721      * @global moodle_database $DB
722      * @uses COMPLETION_COMPLETE
723      * @uses COMPLETION_COMPLETE_FAIL
724      * @uses COMPLETION_COMPLETE_PASS
725      * @param string $information If the item has availability restrictions,
726      *   a string that describes the conditions will be stored in this variable;
727      *   if this variable is set blank, that means don't display anything
728      * @param bool $grabthelot Performance hint: if true, caches information
729      *   required for all course-modules, to make the front page and similar
730      *   pages work more quickly (works only for current user)
731      * @param int $userid If set, specifies a different user ID to check availability for
732      * @param object $modinfo Usually leave as null for default. Specify when
733      *   calling recursively from inside get_fast_modinfo. The value supplied
734      *   here must include list of all CMs with 'id' and 'name'
735      * @return bool True if this item is available to the user, false otherwise
736      */
737     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
738         global $COURSE, $DB;
739         $this->require_data();
741         $available = true;
742         $information = '';
744         // Check each completion condition
745         if (count($this->item->conditionscompletion) > 0) {
746             if ($this->item->course == $COURSE->id) {
747                 $course = $COURSE;
748             } else {
749                 $course = $DB->get_record('course', array('id' => $this->item->course),
750                         'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
751             }
753             $completion = new completion_info($course);
754             foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
755                 // If this depends on a deleted module, handle that situation
756                 // gracefully.
757                 if (!$modinfo) {
758                     $modinfo = get_fast_modinfo($course);
759                 }
760                 if (empty($modinfo->cms[$cmid])) {
761                     global $PAGE, $UNITTEST;
762                     if (!empty($UNITTEST) || (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0)) {
763                         debugging("Warning: activity {$this->cm->id} '{$this->cm->name}' has condition " .
764                                 "on deleted activity $cmid (to get rid of this message, edit the named activity)");
765                     }
766                     continue;
767                 }
769                 // The completion system caches its own data
770                 $completiondata = $completion->get_data((object)array('id' => $cmid),
771                         $grabthelot, $userid, $modinfo);
773                 $thisisok = true;
774                 if ($expectedcompletion==COMPLETION_COMPLETE) {
775                     // 'Complete' also allows the pass, fail states
776                     switch ($completiondata->completionstate) {
777                         case COMPLETION_COMPLETE:
778                         case COMPLETION_COMPLETE_FAIL:
779                         case COMPLETION_COMPLETE_PASS:
780                             break;
781                         default:
782                             $thisisok = false;
783                     }
784                 } else {
785                     // Other values require exact match
786                     if ($completiondata->completionstate!=$expectedcompletion) {
787                         $thisisok = false;
788                     }
789                 }
790                 if (!$thisisok) {
791                     $available = false;
792                     $information .= get_string(
793                         'requires_completion_' . $expectedcompletion,
794                         'condition', $modinfo->cms[$cmid]->name) . ' ';
795                 }
796             }
797         }
799         // Check each grade condition
800         if (count($this->item->conditionsgrade)>0) {
801             foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
802                 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
803                 if ($score===false ||
804                         (!is_null($minmax->min) && $score<$minmax->min) ||
805                         (!is_null($minmax->max) && $score>=$minmax->max)) {
806                     // Grade fail
807                     $available = false;
808                     // String depends on type of requirement. We are coy about
809                     // the actual numbers, in case grades aren't released to
810                     // students.
811                     if (is_null($minmax->min) && is_null($minmax->max)) {
812                         $string = 'any';
813                     } else if (is_null($minmax->max)) {
814                         $string = 'min';
815                     } else if (is_null($minmax->min)) {
816                         $string = 'max';
817                     } else {
818                         $string = 'range';
819                     }
820                     $information .= get_string('requires_grade_' . $string, 'condition', $minmax->name) . ' ';
821                 }
822             }
823         }
825         // Test dates
826         if ($this->item->availablefrom) {
827             if (time() < $this->item->availablefrom) {
828                 $available = false;
830                 $information .= get_string('requires_date', 'condition',
831                         self::show_time($this->item->availablefrom,
832                             self::is_midnight($this->item->availablefrom)));
833             }
834         }
836         if ($this->item->availableuntil) {
837             if (time() >= $this->item->availableuntil) {
838                 $available = false;
839                 // But we don't display any information about this case. This is
840                 // because the only reason to set a 'disappear' date is usually
841                 // to get rid of outdated information/clutter in which case there
842                 // is no point in showing it...
844                 // Note it would be nice if we could make it so that the 'until'
845                 // date appears below the item while the item is still accessible,
846                 // unfortunately this is not possible in the current system. Maybe
847                 // later, or if somebody else wants to add it.
848             }
849         }
851         // If the item is marked as 'not visible' then we don't change the available
852         // flag (visible/available are treated distinctly), but we remove any
853         // availability info. If the item is hidden with the eye icon, it doesn't
854         // make sense to show 'Available from <date>' or similar, because even
855         // when that date arrives it will still not be available unless somebody
856         // toggles the eye icon.
857         if (!$this->item->visible) {
858             $information = '';
859         }
861         $information = trim($information);
862         return $available;
863     }
865     /**
866      * Shows a time either as a date or a full date and time, according to
867      * user's timezone.
868      *
869      * @param int $time Time
870      * @param bool $dateonly If true, uses date only
871      * @return string Date
872      */
873     private function show_time($time, $dateonly) {
874         return userdate($time,
875                 get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
876     }
878     /**
879      * Checks whether availability information should be shown to normal users.
880      *
881      * @return bool True if information about availability should be shown to
882      *   normal users
883      * @throws coding_exception If data wasn't loaded
884      */
885     public function show_availability() {
886         $this->require_data();
887         return $this->item->showavailability;
888     }
890     /**
891      * Internal function cheks that data was loaded.
892      *
893      * @throws coding_exception If data wasn't loaded
894      */
895     private function require_data() {
896         if (!$this->gotdata) {
897             throw new coding_exception('Error: cannot call when info was ' .
898                 'constructed without data');
899         }
900     }
902     /**
903      * Obtains a grade score. Note that this score should not be displayed to
904      * the user, because gradebook rules might prohibit that. It may be a
905      * non-final score subject to adjustment later.
906      *
907      * @global stdClass $USER
908      * @global moodle_database $DB
909      * @global stdClass $SESSION
910      * @param int $gradeitemid Grade item ID we're interested in
911      * @param bool $grabthelot If true, grabs all scores for current user on
912      *   this course, so that later ones come from cache
913      * @param int $userid Set if requesting grade for a different user (does
914      *   not use cache)
915      * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
916      *   or 37.21), or false if user does not have a grade yet
917      */
918     private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
919         global $USER, $DB, $SESSION;
920         if ($userid==0 || $userid==$USER->id) {
921             // For current user, go via cache in session
922             if (empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) {
923                 $SESSION->gradescorecache = array();
924                 $SESSION->gradescorecacheuserid = $USER->id;
925             }
926             if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
927                 if ($grabthelot) {
928                     // Get all grades for the current course
929                     $rs = $DB->get_recordset_sql('
930                             SELECT
931                                 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
932                             FROM
933                                 {grade_items} gi
934                                 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
935                             WHERE
936                                 gi.courseid = ?', array($USER->id, $this->item->course));
937                     foreach ($rs as $record) {
938                         $SESSION->gradescorecache[$record->id] =
939                             is_null($record->finalgrade)
940                                 // No grade = false
941                                 ? false
942                                 // Otherwise convert grade to percentage
943                                 : (($record->finalgrade - $record->rawgrademin) * 100) /
944                                     ($record->rawgrademax - $record->rawgrademin);
946                     }
947                     $rs->close();
948                     // And if it's still not set, well it doesn't exist (eg
949                     // maybe the user set it as a condition, then deleted the
950                     // grade item) so we call it false
951                     if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
952                         $SESSION->gradescorecache[$gradeitemid] = false;
953                     }
954                 } else {
955                     // Just get current grade
956                     $record = $DB->get_record('grade_grades', array(
957                         'userid'=>$USER->id, 'itemid'=>$gradeitemid));
958                     if ($record && !is_null($record->finalgrade)) {
959                         $score = (($record->finalgrade - $record->rawgrademin) * 100) /
960                             ($record->rawgrademax - $record->rawgrademin);
961                     } else {
962                         // Treat the case where row exists but is null, same as
963                         // case where row doesn't exist
964                         $score = false;
965                     }
966                     $SESSION->gradescorecache[$gradeitemid]=$score;
967                 }
968             }
969             return $SESSION->gradescorecache[$gradeitemid];
970         } else {
971             // Not the current user, so request the score individually
972             $record = $DB->get_record('grade_grades', array(
973                 'userid'=>$userid, 'itemid'=>$gradeitemid));
974             if ($record && !is_null($record->finalgrade)) {
975                 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
976                     ($record->rawgrademax - $record->rawgrademin);
977             } else {
978                 // Treat the case where row exists but is null, same as
979                 // case where row doesn't exist
980                 $score = false;
981             }
982             return $score;
983         }
984     }
986     /**
987      * For testing only. Wipes information cached in user session.
988      *
989      * @global stdClass $SESSION
990      */
991     static function wipe_session_cache() {
992         global $SESSION;
993         unset($SESSION->gradescorecache);
994         unset($SESSION->gradescorecacheuserid);
995     }
997     /**
998      * Initialises the global cache
999      * @global stdClass $CONDITIONLIB_PRIVATE
1000      */
1001     public static function init_global_cache() {
1002         global $CONDITIONLIB_PRIVATE;
1003         $CONDITIONLIB_PRIVATE = new stdClass;
1004         $CONDITIONLIB_PRIVATE->usedincondition = array();
1005         $CONDITIONLIB_PRIVATE->groupingscache = array();
1006     }
1008     /**
1009      * Utility function that resets grade/completion conditions in table based
1010      * in data from editing form.
1011      *
1012      * @param condition_info_base $ci Condition info
1013      * @param object $fromform Data from form
1014      * @param bool $wipefirst If true, wipes existing conditions
1015      */
1016     protected static function update_from_form(condition_info_base $ci, $fromform, $wipefirst) {
1017         if ($wipefirst) {
1018             $ci->wipe_conditions();
1019         }
1020         foreach ($fromform->conditiongradegroup as $record) {
1021             if($record['conditiongradeitemid']) {
1022                 $ci->add_grade_condition($record['conditiongradeitemid'],
1023                     unformat_float($record['conditiongrademin']), unformat_float($record['conditiongrademax']));
1024             }
1025         }
1026         if(isset ($fromform->conditioncompletiongroup)) {
1027             foreach($fromform->conditioncompletiongroup as $record) {
1028                 if($record['conditionsourcecmid']) {
1029                     $ci->add_completion_condition($record['conditionsourcecmid'],
1030                         $record['conditionrequiredcompletion']);
1031                 }
1032             }
1033         }
1034     }
1037 condition_info::init_global_cache();