MDL-38364 phpunit: PHPUnit autoload uses capital A
[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 /**
54  * OP_CONTAINS - comparison operator that determines whether a specified user field contains
55  * a provided variable
56  */
57 define('OP_CONTAINS', 'contains');
58 /**
59  * OP_DOES_NOT_CONTAIN - comparison operator that determines whether a specified user field does not
60  * contain a provided variable
61  */
62 define('OP_DOES_NOT_CONTAIN', 'doesnotcontain');
63 /**
64  * OP_IS_EQUAL_TO - comparison operator that determines whether a specified user field is equal to
65  * a provided variable
66  */
67 define('OP_IS_EQUAL_TO', 'isequalto');
68 /**
69  * OP_STARTS_WITH - comparison operator that determines whether a specified user field starts with
70  * a provided variable
71  */
72 define('OP_STARTS_WITH', 'startswith');
73 /**
74  * OP_ENDS_WITH - comparison operator that determines whether a specified user field ends with
75  * a provided variable
76  */
77 define('OP_ENDS_WITH', 'endswith');
78 /**
79  * OP_IS_EMPTY - comparison operator that determines whether a specified user field is empty
80  */
81 define('OP_IS_EMPTY', 'isempty');
82 /**
83  * OP_IS_NOT_EMPTY - comparison operator that determines whether a specified user field is not empty
84  */
85 define('OP_IS_NOT_EMPTY', 'isnotempty');
87 require_once($CFG->libdir.'/completionlib.php');
89 /**
90  * Core class to handle conditional activites
91  *
92  * @package   core_condition
93  * @category  condition
94  * @copyright 2012 The Open University
95  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
96  */
97 class condition_info extends condition_info_base {
98     /**
99      * Constructs with course-module details.
100      *
101      * @global moodle_database $DB
102      * @uses CONDITION_MISSING_NOTHING
103      * @param object $cm Moodle course-module object. May have extra fields
104      *   ->conditionsgrade, ->conditionscompletion which should come from
105      *   get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
106      *   and ->showavailability, ->course, ->visible; but the only required
107      *   thing is ->id.
108      * @param int $expectingmissing Used to control whether or not a developer
109      *   debugging message (performance warning) will be displayed if some of
110      *   the above data is missing and needs to be retrieved; a
111      *   CONDITION_MISSING_xx constant
112      * @param bool $loaddata If you need a 'write-only' object, set this value
113      *   to false to prevent database access from constructor
114      */
115     public function __construct($cm, $expectingmissing = CONDITION_MISSING_NOTHING,
116         $loaddata=true) {
117         parent::__construct($cm, 'course_modules', 'coursemoduleid',
118                 $expectingmissing, $loaddata);
119     }
121     /**
122      * Adds the extra availability conditions (if any) into the given
123      * course-module (or section) object.
124      *
125      * This function may be called statically (for the editing form) or
126      * dynamically.
127      *
128      * @param object $cm Moodle course-module data object
129      */
130     public static function fill_availability_conditions($cm) {
131         parent::fill_availability_conditions_inner($cm, 'course_modules', 'coursemoduleid');
132     }
134     /**
135      * Gets the course-module object with full necessary data to determine availability.
136      * @return object Course-module object with full data
137      * @throws coding_exception If data was not supplied when constructing object
138      */
139     public function get_full_course_module() {
140         return $this->get_full_item();
141     }
143     /**
144      * Utility function called by modedit.php; updates the
145      * course_modules_availability table based on the module form data.
146      *
147      * @param object $cm Course-module with as much data as necessary, min id
148      * @param object $fromform Data from form
149      * @param bool $wipefirst If true, wipes existing conditions
150      */
151     public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
152         $ci = new condition_info($cm, CONDITION_MISSING_EVERYTHING, false);
153         parent::update_from_form($ci, $fromform, $wipefirst);
154     }
156     /**
157      * Used in course/lib.php because we need to disable the completion JS if
158      * a completion value affects a conditional activity.
159      *
160      * @global stdClass $CONDITIONLIB_PRIVATE
161      * @param object $course Moodle course object
162      * @param object $item Moodle course-module
163      * @return bool True if this is used in a condition, false otherwise
164      */
165     public static function completion_value_used_as_condition($course, $cm) {
166         // Have we already worked out a list of required completion values
167         // for this course? If so just use that
168         global $CONDITIONLIB_PRIVATE, $DB;
169         if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) {
170             // We don't have data for this course, build it
171             $modinfo = get_fast_modinfo($course);
172             $CONDITIONLIB_PRIVATE->usedincondition[$course->id] = array();
174             // Activities
175             foreach ($modinfo->cms as $othercm) {
176                 foreach ($othercm->conditionscompletion as $cmid => $expectedcompletion) {
177                     $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
178                 }
179             }
181             // Sections
182             foreach ($modinfo->get_section_info_all() as $section) {
183                 foreach ($section->conditionscompletion as $cmid => $expectedcompletion) {
184                     $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
185                 }
186             }
187         }
188         return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]);
189     }
193 /**
194  * Handles conditional access to sections.
195  *
196  * @package   core_condition
197  * @category  condition
198  * @copyright 2012 The Open University
199  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
200  */
201 class condition_info_section extends condition_info_base {
202     /**
203      * Constructs with course-module details.
204      *
205      * @global moodle_database $DB
206      * @uses CONDITION_MISSING_NOTHING
207      * @param object $section Moodle section object. May have extra fields
208      *   ->conditionsgrade, ->conditionscompletion. Should have ->availablefrom,
209      *   ->availableuntil, and ->showavailability, ->course; but the only
210      *   required thing is ->id.
211      * @param int $expectingmissing Used to control whether or not a developer
212      *   debugging message (performance warning) will be displayed if some of
213      *   the above data is missing and needs to be retrieved; a
214      *   CONDITION_MISSING_xx constant
215      * @param bool $loaddata If you need a 'write-only' object, set this value
216      *   to false to prevent database access from constructor
217      */
218     public function __construct($section, $expectingmissing = CONDITION_MISSING_NOTHING,
219         $loaddata=true) {
220         parent::__construct($section, 'course_sections', 'coursesectionid',
221                 $expectingmissing, $loaddata);
222     }
224     /**
225      * Adds the extra availability conditions (if any) into the given
226      * course-module (or section) object.
227      *
228      * This function may be called statically (for the editing form) or
229      * dynamically.
230      *
231      * @param object $section Moodle section data object
232      */
233     public static function fill_availability_conditions($section) {
234         parent::fill_availability_conditions_inner($section, 'course_sections', 'coursesectionid');
235     }
237     /**
238      * Gets the section object with full necessary data to determine availability.
239      * @return object Section object with full data
240      * @throws coding_exception If data was not supplied when constructing object
241      */
242     public function get_full_section() {
243         return $this->get_full_item();
244     }
246     /**
247      * Gets list of required fields from main table.
248      * @return array Array of field names
249      */
250     protected function get_main_table_fields() {
251         return array_merge(parent::get_main_table_fields(), array('groupingid'));
252     }
254     /**
255      * Determines whether this particular section is currently available
256      * according to these criteria.
257      *
258      * - This does not include the 'visible' setting (i.e. this might return
259      *   true even if visible is false); visible is handled independently.
260      * - This does not take account of the viewhiddenactivities capability.
261      *   That should apply later.
262      *
263      * @global moodle_database $DB
264      * @global stdclass $USER
265      * @param string $information If the item has availability restrictions,
266      *   a string that describes the conditions will be stored in this variable;
267      *   if this variable is set blank, that means don't display anything
268      * @param bool $grabthelot Performance hint: if true, caches information
269      *   required for all course-modules, to make the front page and similar
270      *   pages work more quickly (works only for current user)
271      * @param int $userid If set, specifies a different user ID to check availability for
272      * @param object $modinfo Usually leave as null for default. Specify when
273      *   calling recursively from inside get_fast_modinfo. The value supplied
274      *   here must include list of all CMs with 'id' and 'name'
275      * @return bool True if this item is available to the user, false otherwise
276      */
277     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
278         global $DB, $USER, $CONDITIONLIB_PRIVATE;
280         $available = parent::is_available($information, $grabthelot, $userid, $modinfo);
282         // test if user is enrolled to a grouping which has access to the section
283         if (!empty($this->item->groupingid)) {
284             // Get real user id
285             if (!$userid) {
286                 $userid = $USER->id;
287             }
288             $context = context_course::instance($this->item->course);
290             if ($userid != $USER->id) {
291                 // We are requesting for a non-current user so check it individually
292                 // (no cache). Do grouping check first, it's probably faster
293                 // than the capability check
294                 $gotit = $DB->record_exists_sql('
295                         SELECT
296                             1
297                         FROM
298                             {groupings} g
299                             JOIN {groupings_groups} gg ON g.id = gg.groupingid
300                             JOIN {groups_members} gm ON gg.groupid = gm.groupid
301                         WHERE
302                             g.id = ? AND gm.userid = ?',
303                         array($this->item->groupingid, $userid));
304                 if (!$gotit && !has_capability('moodle/site:accessallgroups', $context, $userid)) {
305                     $available = false;
306                     $information .= get_string('groupingnoaccess', 'condition');
307                 }
308             } else {
309                 // Request is for current user - use cache
310                 if( !array_key_exists($this->item->course, $CONDITIONLIB_PRIVATE->groupingscache)) {
311                     if (has_capability('moodle/site:accessallgroups', $context)) {
312                         $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = true;
313                     } else {
314                         $groupings = $DB->get_records_sql('
315                                 SELECT
316                                     g.id as gid
317                                 FROM
318                                     {groupings} g
319                                     JOIN {groupings_groups} gg ON g.id = gg.groupingid
320                                     JOIN {groups_members} gm ON gg.groupid = gm.groupid
321                                 WHERE
322                                     g.courseid = ? AND gm.userid = ?',
323                                 array($this->item->course, $userid));
324                         $list = array();
325                         foreach ($groupings as $grouping) {
326                             $list[$grouping->gid] = true;
327                         }
328                         $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = $list;
329                     }
330                 }
332                 $usergroupings = $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course];
333                 if ($usergroupings !== true && !array_key_exists($this->item->groupingid, $usergroupings)) {
334                     $available = false;
335                     $information .= get_string('groupingnoaccess', 'condition');
336                 }
337             }
338         }
340         $information = trim($information);
341         return $available;
342     }
344     /**
345      * Utility function called by modedit.php; updates the
346      * course_modules_availability table based on the module form data.
347      *
348      * @param object $section Section object, must at minimum contain id
349      * @param object $fromform Data from form
350      * @param bool $wipefirst If true, wipes existing conditions
351      */
352     public static function update_section_from_form($section, $fromform, $wipefirst=true) {
353         $ci = new condition_info_section($section, CONDITION_MISSING_EVERYTHING);
354         parent::update_from_form($ci, $fromform, $wipefirst);
355     }
359 /**
360  * Base class to handle conditional items of some kind (currently either
361  * course_modules or sections; they must have a corresponding _availability
362  * table).
363  *
364  * @package   core_condition
365  * @category  condition
366  * @copyright 2012 The Open University
367  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
368  */
369 abstract class condition_info_base {
370     protected $item;
371     /** @var bool */
372     protected $gotdata;
373     /** @var string */
374     protected $availtable;
375     /** @var string */
376     protected $availfieldtable;
377     /** @var string */
378     protected $idfieldname;
379     /** @var array */
380     protected $usergroupings;
381     /** @var array An array of custom profile field ids => to their shortname */
382     protected $customprofilefields = null;
383     /**
384      * Constructs with item details.
385      *
386      * @global moodle_database $DB
387      * @uses CONDITION_MISSING_NOTHING
388      * @uses CONDITION_MISSING_EVERYTHING
389      * @uses CONDITION_MISSING_EXTRATABLE
390      * @uses DEBUG_DEVELOPER
391      * @param object $item Object representing some kind of item (cm or section).
392      *   May have extra fields ->conditionsgrade, ->conditionscompletion.
393      *   Should have ->availablefrom, ->availableuntil, and ->showavailability,
394      *   ->course; but the only required thing is ->id.
395      * @param string $tableprefix Prefix for table used to store availability
396      *   data, e.g. 'course_modules' if we are going to look at
397      *   course_modules_availability.
398      * @param string $idfield Within this table, name of field used as item id
399      *   (e.g. 'coursemoduleid')
400      * @param int $expectingmissing Used to control whether or not a developer
401      *   debugging message (performance warning) will be displayed if some of
402      *   the above data is missing and needs to be retrieved; a
403      *   CONDITION_MISSING_xx constant
404      * @param bool $loaddata If you need a 'write-only' object, set this value
405      *   to false to prevent database access from constructor
406      * @return condition_info Object which can retrieve information about the
407      *   activity
408      */
409     public function __construct($item, $tableprefix, $idfield, $expectingmissing, $loaddata) {
410         global $DB;
412         // Check ID as otherwise we can't do the other queries
413         if (empty($item->id)) {
414             throw new coding_exception('Invalid parameters; item ID not included');
415         }
417         // DB table to store availability conditions
418         $this->availtable = $tableprefix . '_availability';
420         // DB table to store availability conditions for user fields
421         $this->availfieldtable = $tableprefix . '_avail_fields';
423         // Name of module/section ID field in DB
424         $this->idfieldname = $idfield;
426         // If not loading data, don't do anything else
427         if (!$loaddata) {
428             $this->item = (object)array('id' => $item->id);
429             $this->gotdata = false;
430             return;
431         }
433         // Missing basic data from course_modules
434         $basicfields = $this->get_main_table_fields();
435         $missingbasicfields = false;
436         foreach ($basicfields as $field) {
437             if (!isset($item->{$field})) {
438                 $missingbasicfields = true;
439                 break;
440             }
441         }
442         if ($missingbasicfields) {
443             if ($expectingmissing<CONDITION_MISSING_EVERYTHING) {
444                 debugging('Performance warning: condition_info constructor is ' .
445                         'faster if you pass in $item with at least basic fields ' .
446                         'from its table. '.
447                         '[This warning can be disabled, see phpdoc.]',
448                         DEBUG_DEVELOPER);
449             }
450             $item = $DB->get_record($tableprefix, array('id' => $item->id),
451                     implode(',', $basicfields), MUST_EXIST);
452         }
454         $this->item = clone($item);
455         $this->gotdata = true;
457         // Missing extra data
458         if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion) || !isset($item->conditionsfield)) {
459             if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) {
460                 debugging('Performance warning: condition_info constructor is ' .
461                         'faster if you pass in a $item from get_fast_modinfo or ' .
462                         'the equivalent for sections. ' .
463                         '[This warning can be disabled, see phpdoc.]',
464                         DEBUG_DEVELOPER);
465             }
467             $this->fill_availability_conditions($this->item);
468         }
469     }
471     /**
472      * Gets list of required fields from main table.
473      *
474      * @return array Array of field names
475      */
476     protected function get_main_table_fields() {
477         return array('id', 'course', 'visible',
478                 'availablefrom', 'availableuntil', 'showavailability');
479     }
481     /**
482      * Fills availability conditions into the item object, if they are missing,
483      * otherwise does nothing. Called by subclass fill_availability_conditions.
484      * @param object $item Item object
485      * @param string $tableprefix Prefix of name for _availability table e.g. 'course_modules'
486      * @param string $idfield Name of field that contains id e.g. 'coursemoduleid'
487      * @throws coding_exception If item object doesn't have id field
488      */
489     protected static function fill_availability_conditions_inner($item, $tableprefix, $idfield) {
490         global $DB, $CFG;
491         if (empty($item->id)) {
492             throw new coding_exception('Invalid parameters; item ID not included');
493         }
495         // Does nothing if the variables are already present
496         if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion) || !isset($item->conditionsfield)) {
497             $item->conditionsgrade = array();
498             $item->conditionscompletion = array();
499             $item->conditionsfield = array();
501             $conditions = $DB->get_records_sql('
502                     SELECT
503                         a.id AS aid, gi.*, a.sourcecmid, a.requiredcompletion, a.gradeitemid,
504                         a.grademin as conditiongrademin, a.grademax as conditiongrademax
505                     FROM
506                         {' . $tableprefix . '_availability} a
507                         LEFT JOIN {grade_items} gi ON gi.id = a.gradeitemid
508                     WHERE ' . $idfield . ' = ?', array($item->id));
509             foreach ($conditions as $condition) {
510                 if (!is_null($condition->sourcecmid)) {
511                     $item->conditionscompletion[$condition->sourcecmid] =
512                         $condition->requiredcompletion;
513                 } else {
514                     $minmax = new stdClass;
515                     $minmax->min = $condition->conditiongrademin;
516                     $minmax->max = $condition->conditiongrademax;
517                     $minmax->name = self::get_grade_name($condition);
518                     $item->conditionsgrade[$condition->gradeitemid] = $minmax;
519                 }
520             }
522             // For user fields
523             $sql = "SELECT a.id as cmaid, a.*, uf.*
524                       FROM {" . $tableprefix . "_avail_fields} a
525                  LEFT JOIN {user_info_field} uf ON a.customfieldid =  uf.id
526                      WHERE " . $idfield . " = :itemid";
527             $conditions = $DB->get_records_sql($sql, array('itemid' => $item->id));
528             foreach ($conditions as $condition) {
529                 // If the custom field is not empty, then we have a custom profile field
530                 if (!empty($condition->customfieldid)) {
531                     $field = $condition->customfieldid;
532                     // Check if the profile field name is not empty, if it is
533                     // then the custom profile field no longer exists, so
534                     // display !missing instead.
535                     if (!empty($condition->name)) {
536                         $fieldname = $condition->name;
537                     } else {
538                         $fieldname = '!missing';
539                     }
540                 } else {
541                     $field = $condition->userfield;
542                     $fieldname = $condition->userfield;
543                 }
544                 $details = new stdClass;
545                 $details->fieldname = $fieldname;
546                 $details->operator = $condition->operator;
547                 $details->value = $condition->value;
548                 $item->conditionsfield[$field] = $details;
549             }
550         }
551     }
553     /**
554      * Obtains the name of a grade item.
555      *
556      * @global object $CFG
557      * @param object $gradeitemobj Object from get_record on grade_items table,
558      *     (can be empty if you want to just get !missing)
559      * @return string Name of item of !missing if it didn't exist
560      */
561     private static function get_grade_name($gradeitemobj) {
562         global $CFG;
563         if (isset($gradeitemobj->id)) {
564             require_once($CFG->libdir . '/gradelib.php');
565             $item = new grade_item;
566             grade_object::set_properties($item, $gradeitemobj);
567             return $item->get_name();
568         } else {
569             return '!missing'; // Ooops, missing grade
570         }
571     }
573     /**
574      * Gets the item object with full necessary data to determine availability.
575      * @return object Item object with full data
576      * @throws coding_exception If data was not supplied when constructing object
577      */
578     protected function get_full_item() {
579         $this->require_data();
580         return $this->item;
581     }
583     /**
584      * The operators that provide the relationship
585      * between a field and a value.
586      *
587      * @return array Associative array from operator constant to display name
588      */
589     public static function get_condition_user_field_operators() {
590         return array(
591             OP_CONTAINS => get_string('contains', 'condition'),
592             OP_DOES_NOT_CONTAIN => get_string('doesnotcontain', 'condition'),
593             OP_IS_EQUAL_TO => get_string('isequalto', 'condition'),
594             OP_STARTS_WITH => get_string('startswith', 'condition'),
595             OP_ENDS_WITH => get_string('endswith', 'condition'),
596             OP_IS_EMPTY => get_string('isempty', 'condition'),
597             OP_IS_NOT_EMPTY => get_string('isnotempty', 'condition'),
598         );
599     }
601     /**
602      * Returns list of user fields that can be compared.
603      *
604      * If you specify $formatoptions, then format_string will be called on the
605      * custom field names. This is necessary for multilang support to work so
606      * you should include this parameter unless you are going to format the
607      * text later.
608      *
609      * @param array $formatoptions Passed to format_string if provided
610      * @return array Associative array from user field constants to display name
611      */
612     public static function get_condition_user_fields($formatoptions = null) {
613         global $DB;
615         $userfields = array(
616             'firstname' => get_user_field_name('firstname'),
617             'lastname' => get_user_field_name('lastname'),
618             'email' => get_user_field_name('email'),
619             'city' => get_user_field_name('city'),
620             'country' => get_user_field_name('country'),
621             'url' => get_user_field_name('url'),
622             'icq' => get_user_field_name('icq'),
623             'skype' => get_user_field_name('skype'),
624             'aim' => get_user_field_name('aim'),
625             'yahoo' => get_user_field_name('yahoo'),
626             'msn' => get_user_field_name('msn'),
627             'idnumber' => get_user_field_name('idnumber'),
628             'institution' => get_user_field_name('institution'),
629             'department' => get_user_field_name('department'),
630             'phone1' => get_user_field_name('phone1'),
631             'phone2' => get_user_field_name('phone2'),
632             'address' => get_user_field_name('address')
633         );
635         // Go through the custom profile fields now
636         if ($user_info_fields = $DB->get_records('user_info_field')) {
637             foreach ($user_info_fields as $field) {
638                 if ($formatoptions) {
639                     $userfields[$field->id] = format_string($field->name, true, $formatoptions);
640                 } else {
641                     $userfields[$field->id] = $field->name;
642                 }
643             }
644         }
646         return $userfields;
647     }
649     /**
650      * Adds to the database a condition based on completion of another module.
651      *
652      * @global moodle_database $DB
653      * @param int $cmid ID of other module
654      * @param int $requiredcompletion COMPLETION_xx constant
655      */
656     public function add_completion_condition($cmid, $requiredcompletion) {
657         global $DB;
658         // Add to DB
659         $DB->insert_record($this->availtable, (object)array(
660                 $this->idfieldname => $this->item->id,
661                 'sourcecmid' => $cmid, 'requiredcompletion' => $requiredcompletion),
662                 false);
664         // Store in memory too
665         $this->item->conditionscompletion[$cmid] = $requiredcompletion;
666     }
668     /**
669      * Adds user fields condition
670      *
671      * @param mixed $field numeric if it is a user profile field, character
672      *                     if it is a column in the user table
673      * @param int $operator specifies the relationship between field and value
674      * @param char $value the value of the field
675      */
676     public function add_user_field_condition($field, $operator, $value) {
677         global $DB;
679         // Get the field name
680         $idfieldname = $this->idfieldname;
682         $objavailfield = new stdClass;
683         $objavailfield->$idfieldname = $this->item->id;
684         if (is_numeric($field)) { // If the condition field is numeric then it is a custom profile field
685             // Need to get the field name so we can add it to the cache
686             $ufield = $DB->get_field('user_info_field', 'name', array('id' => $field));
687             $objavailfield->fieldname = $ufield;
688             $objavailfield->customfieldid = $field;
689         } else {
690             $objavailfield->fieldname = $field;
691             $objavailfield->userfield = $field;
692         }
693         $objavailfield->operator = $operator;
694         $objavailfield->value = $value;
695         $DB->insert_record($this->availfieldtable, $objavailfield, false);
697         // Store in memory too
698         $this->item->conditionsfield[$field] = $objavailfield;
699     }
701     /**
702      * Adds to the database a condition based on the value of a grade item.
703      *
704      * @global moodle_database $DB
705      * @param int $gradeitemid ID of grade item
706      * @param float $min Minimum grade (>=), up to 5 decimal points, or null if none
707      * @param float $max Maximum grade (<), up to 5 decimal points, or null if none
708      * @param bool $updateinmemory If true, updates data in memory; otherwise,
709      *   memory version may be out of date (this has performance consequences,
710      *   so don't do it unless it really needs updating)
711      */
712     public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
713         global $DB;
714         // Normalise nulls
715         if ($min==='') {
716             $min = null;
717         }
718         if ($max==='') {
719             $max = null;
720         }
721         // Add to DB
722         $DB->insert_record($this->availtable, (object)array(
723                 $this->idfieldname => $this->item->id,
724                 'gradeitemid' => $gradeitemid, 'grademin' => $min, 'grademax' => $max),
725                 false);
727         // Store in memory too
728         if ($updateinmemory) {
729             $this->item->conditionsgrade[$gradeitemid] = (object) array(
730                     'min' => $min, 'max' => $max);
731             $this->item->conditionsgrade[$gradeitemid]->name = self::get_grade_name(
732                     $DB->get_record('grade_items', array('id'=>$gradeitemid)));
733         }
734     }
736      /**
737      * Erases from the database all conditions for this activity.
738      *
739      * @global moodle_database $DB
740      */
741     public function wipe_conditions() {
742         // Wipe from DB
743         global $DB;
745         $DB->delete_records($this->availtable, array($this->idfieldname => $this->item->id));
746         $DB->delete_records($this->availfieldtable, array($this->idfieldname => $this->item->id));
748         // And from memory
749         $this->item->conditionsgrade = array();
750         $this->item->conditionscompletion = array();
751         $this->item->conditionsfield = array();
752     }
754     /**
755      * Obtains a string describing all availability restrictions (even if
756      * they do not apply any more).
757      *
758      * @global stdClass $COURSE
759      * @global moodle_database $DB
760      * @param object $modinfo Usually leave as null for default. Specify when
761      *   calling recursively from inside get_fast_modinfo. The value supplied
762      *   here must include list of all CMs with 'id' and 'name'
763      * @return string Information string (for admin) about all restrictions on
764      *   this item
765      */
766     public function get_full_information($modinfo=null) {
767         global $COURSE, $DB;
768         $this->require_data();
770         $information = '';
773         // Completion conditions
774         if (count($this->item->conditionscompletion) > 0) {
775             if ($this->item->course == $COURSE->id) {
776                 $course = $COURSE;
777             } else {
778                 $course = $DB->get_record('course', array('id' => $this->item->course),
779                         'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
780             }
781             foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
782                 if (!$modinfo) {
783                     $modinfo = get_fast_modinfo($course);
784                 }
785                 if (empty($modinfo->cms[$cmid])) {
786                     continue;
787                 }
788                 $information .= html_writer::start_tag('li');
789                 $information .= get_string(
790                         'requires_completion_' . $expectedcompletion,
791                         'condition', $modinfo->cms[$cmid]->name) . ' ';
792                 $information .= html_writer::end_tag('li');
793             }
794         }
796         // Grade conditions
797         if (count($this->item->conditionsgrade) > 0) {
798             foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
799                 // String depends on type of requirement. We are coy about
800                 // the actual numbers, in case grades aren't released to
801                 // students.
802                 if (is_null($minmax->min) && is_null($minmax->max)) {
803                     $string = 'any';
804                 } else if (is_null($minmax->max)) {
805                     $string = 'min';
806                 } else if (is_null($minmax->min)) {
807                     $string = 'max';
808                 } else {
809                     $string = 'range';
810                 }
811                 $information .= html_writer::start_tag('li');
812                 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
813                 $information .= html_writer::end_tag('li');
814             }
815         }
817         // User field conditions
818         if (count($this->item->conditionsfield) > 0) {
819             // Need the array of operators
820             foreach ($this->item->conditionsfield as $field => $details) {
821                 $a = new stdclass;
822                 $a->field = $details->fieldname;
823                 $a->value = $details->value;
824                 $information .= html_writer::start_tag('li');
825                 $information .= get_string('requires_user_field_'.$details->operator, 'condition', $a) . ' ';
826                 $information .= html_writer::end_tag('li');
827             }
828         }
830         // The date logic is complicated. The intention of this logic is:
831         // 1) display date without time where possible (whenever the date is
832         //    midnight)
833         // 2) when the 'until' date is e.g. 00:00 on the 14th, we display it as
834         //    'until the 13th' (experience at the OU showed that students are
835         //    likely to interpret 'until <date>' as 'until the end of <date>').
836         // 3) This behaviour becomes confusing for 'same-day' dates where there
837         //    are some exceptions.
838         // Users in different time zones will typically not get the 'abbreviated'
839         // behaviour but it should work OK for them aside from that.
841         // The following cases are possible:
842         // a) From 13:05 on 14 Oct until 12:10 on 17 Oct (exact, exact)
843         // b) From 14 Oct until 12:11 on 17 Oct (midnight, exact)
844         // c) From 13:05 on 14 Oct until 17 Oct (exact, midnight 18 Oct)
845         // d) From 14 Oct until 17 Oct (midnight 14 Oct, midnight 18 Oct)
846         // e) On 14 Oct (midnight 14 Oct, midnight 15 Oct)
847         // f) From 13:05 on 14 Oct until 0:00 on 15 Oct (exact, midnight, same day)
848         // g) From 0:00 on 14 Oct until 12:05 on 14 Oct (midnight, exact, same day)
849         // h) From 13:05 on 14 Oct (exact)
850         // i) From 14 Oct (midnight)
851         // j) Until 13:05 on 14 Oct (exact)
852         // k) Until 14 Oct (midnight 15 Oct)
854         // Check if start and end dates are 'midnights', if so we show in short form
855         $shortfrom = self::is_midnight($this->item->availablefrom);
856         $shortuntil = self::is_midnight($this->item->availableuntil);
858         // For some checks and for display, we need the previous day for the 'until'
859         // value, if we are going to display it in short form
860         if ($this->item->availableuntil) {
861             $daybeforeuntil = strtotime('-1 day', usergetmidnight($this->item->availableuntil));
862         }
864         // Special case for if one but not both are exact and they are within a day
865         if ($this->item->availablefrom && $this->item->availableuntil &&
866                 $shortfrom != $shortuntil && $daybeforeuntil < $this->item->availablefrom) {
867             // Don't use abbreviated version (see examples f, g above)
868             $shortfrom = false;
869             $shortuntil = false;
870         }
872         // When showing short end date, the display time is the 'day before' one
873         $displayuntil = $shortuntil ? $daybeforeuntil : $this->item->availableuntil;
875         if ($this->item->availablefrom && $this->item->availableuntil) {
876             if ($shortfrom && $shortuntil && $daybeforeuntil == $this->item->availablefrom) {
877                 $information .= html_writer::start_tag('li');
878                 $information .= get_string('requires_date_both_single_day', 'condition',
879                         self::show_time($this->item->availablefrom, true));
880                 $information .= html_writer::end_tag('li');
881             } else {
882                 $information .= html_writer::start_tag('li');
883                 $information .= get_string('requires_date_both', 'condition', (object)array(
884                          'from' => self::show_time($this->item->availablefrom, $shortfrom),
885                          'until' => self::show_time($displayuntil, $shortuntil)));
886                 $information .= html_writer::end_tag('li');
887             }
888         } else if ($this->item->availablefrom) {
889             $information .= html_writer::start_tag('li');
890             $information .= get_string('requires_date', 'condition',
891                 self::show_time($this->item->availablefrom, $shortfrom));
892             $information .= html_writer::end_tag('li');
893         } else if ($this->item->availableuntil) {
894             $information .= html_writer::start_tag('li');
895             $information .= get_string('requires_date_before', 'condition',
896                 self::show_time($displayuntil, $shortuntil));
897             $information .= html_writer::end_tag('li');
898         }
900         // The information is in <li> tags, but to avoid taking up more space
901         // if there is only a single item, we strip out the list tags so that it
902         // is plain text in that case.
903         if (!empty($information)) {
904             $li = strpos($information, '<li>', 4);
905             if ($li === false) {
906                 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
907             } else {
908                 $information = html_writer::tag('ul', $information);
909             }
910             $information = trim($information);
911         }
912         return $information;
913     }
915     /**
916      * Checks whether a given time refers exactly to midnight (in current user
917      * timezone).
918      *
919      * @param int $time Time
920      * @return bool True if time refers to midnight, false if it's some other
921      *   time or if it is set to zero
922      */
923     private static function is_midnight($time) {
924         return $time && usergetmidnight($time) == $time;
925     }
927     /**
928      * Determines whether this particular item is currently available
929      * according to these criteria.
930      *
931      * - This does not include the 'visible' setting (i.e. this might return
932      *   true even if visible is false); visible is handled independently.
933      * - This does not take account of the viewhiddenactivities capability.
934      *   That should apply later.
935      *
936      * @global stdClass $COURSE
937      * @global moodle_database $DB
938      * @uses COMPLETION_COMPLETE
939      * @uses COMPLETION_COMPLETE_FAIL
940      * @uses COMPLETION_COMPLETE_PASS
941      * @param string $information If the item has availability restrictions,
942      *   a string that describes the conditions will be stored in this variable;
943      *   if this variable is set blank, that means don't display anything
944      * @param bool $grabthelot Performance hint: if true, caches information
945      *   required for all course-modules, to make the front page and similar
946      *   pages work more quickly (works only for current user)
947      * @param int $userid If set, specifies a different user ID to check availability for
948      * @param object $modinfo Usually leave as null for default. Specify when
949      *   calling recursively from inside get_fast_modinfo. The value supplied
950      *   here must include list of all CMs with 'id' and 'name'
951      * @return bool True if this item is available to the user, false otherwise
952      */
953     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
954         global $COURSE, $DB;
955         $this->require_data();
957         $available = true;
958         $information = '';
960         // Check each completion condition
961         if (count($this->item->conditionscompletion) > 0) {
962             if ($this->item->course == $COURSE->id) {
963                 $course = $COURSE;
964             } else {
965                 $course = $DB->get_record('course', array('id' => $this->item->course),
966                         'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
967             }
969             $completion = new completion_info($course);
970             foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
971                 // If this depends on a deleted module, handle that situation
972                 // gracefully.
973                 if (!$modinfo) {
974                     $modinfo = get_fast_modinfo($course);
975                 }
976                 if (empty($modinfo->cms[$cmid])) {
977                     global $PAGE;
978                     if (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0) {
979                         debugging("Warning: activity {$this->item->id} '{$this->item->name}' has condition " .
980                                 "on deleted activity $cmid (to get rid of this message, edit the named activity)");
981                     }
982                     continue;
983                 }
985                 // The completion system caches its own data
986                 $completiondata = $completion->get_data((object)array('id' => $cmid),
987                         $grabthelot, $userid, $modinfo);
989                 $thisisok = true;
990                 if ($expectedcompletion==COMPLETION_COMPLETE) {
991                     // 'Complete' also allows the pass, fail states
992                     switch ($completiondata->completionstate) {
993                         case COMPLETION_COMPLETE:
994                         case COMPLETION_COMPLETE_FAIL:
995                         case COMPLETION_COMPLETE_PASS:
996                             break;
997                         default:
998                             $thisisok = false;
999                     }
1000                 } else {
1001                     // Other values require exact match
1002                     if ($completiondata->completionstate!=$expectedcompletion) {
1003                         $thisisok = false;
1004                     }
1005                 }
1006                 if (!$thisisok) {
1007                     $available = false;
1008                     $information .= html_writer::start_tag('li');
1009                     $information .= get_string(
1010                         'requires_completion_' . $expectedcompletion,
1011                         'condition', $modinfo->cms[$cmid]->name) . ' ';
1012                     $information .= html_writer::end_tag('li');
1013                 }
1014             }
1015         }
1017         // Check each grade condition
1018         if (count($this->item->conditionsgrade)>0) {
1019             foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
1020                 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
1021                 if ($score===false ||
1022                         (!is_null($minmax->min) && $score<$minmax->min) ||
1023                         (!is_null($minmax->max) && $score>=$minmax->max)) {
1024                     // Grade fail
1025                     $available = false;
1026                     // String depends on type of requirement. We are coy about
1027                     // the actual numbers, in case grades aren't released to
1028                     // students.
1029                     if (is_null($minmax->min) && is_null($minmax->max)) {
1030                         $string = 'any';
1031                     } else if (is_null($minmax->max)) {
1032                         $string = 'min';
1033                     } else if (is_null($minmax->min)) {
1034                         $string = 'max';
1035                     } else {
1036                         $string = 'range';
1037                     }
1038                     $information .= html_writer::start_tag('li');
1039                     $information .= get_string('requires_grade_' . $string, 'condition', $minmax->name) . ' ';
1040                     $information .= html_writer::end_tag('li');
1041                 }
1042             }
1043         }
1045         // Check if user field condition
1046         if (count($this->item->conditionsfield) > 0) {
1047             foreach ($this->item->conditionsfield as $field => $details) {
1048                 $uservalue = $this->get_cached_user_profile_field($userid, $field);
1049                 if (!$this->is_field_condition_met($details->operator, $uservalue, $details->value)) {
1050                     // Set available to false
1051                     $available = false;
1052                     $a = new stdClass();
1053                     $a->field = $details->fieldname;
1054                     $a->value = $details->value;
1055                     $information .= html_writer::start_tag('li');
1056                     $information .= get_string('requires_user_field_'.$details->operator, 'condition', $a) . ' ';
1057                     $information .= html_writer::end_tag('li');
1058                 }
1059             }
1060         }
1062         // Test dates
1063         if ($this->item->availablefrom) {
1064             if (time() < $this->item->availablefrom) {
1065                 $available = false;
1067                 $information .= html_writer::start_tag('li');
1068                 $information .= get_string('requires_date', 'condition',
1069                         self::show_time($this->item->availablefrom,
1070                             self::is_midnight($this->item->availablefrom)));
1071                 $information .= html_writer::end_tag('li');
1072             }
1073         }
1075         if ($this->item->availableuntil) {
1076             if (time() >= $this->item->availableuntil) {
1077                 $available = false;
1078                 // But we don't display any information about this case. This is
1079                 // because the only reason to set a 'disappear' date is usually
1080                 // to get rid of outdated information/clutter in which case there
1081                 // is no point in showing it...
1083                 // Note it would be nice if we could make it so that the 'until'
1084                 // date appears below the item while the item is still accessible,
1085                 // unfortunately this is not possible in the current system. Maybe
1086                 // later, or if somebody else wants to add it.
1087             }
1088         }
1090         // If the item is marked as 'not visible' then we don't change the available
1091         // flag (visible/available are treated distinctly), but we remove any
1092         // availability info. If the item is hidden with the eye icon, it doesn't
1093         // make sense to show 'Available from <date>' or similar, because even
1094         // when that date arrives it will still not be available unless somebody
1095         // toggles the eye icon.
1096         if (!$this->item->visible) {
1097             $information = '';
1098         }
1100         // The information is in <li> tags, but to avoid taking up more space
1101         // if there is only a single item, we strip out the list tags so that it
1102         // is plain text in that case.
1103         if (!empty($information)) {
1104             $li = strpos($information, '<li>', 4);
1105             if ($li === false) {
1106                 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
1107             } else {
1108                 $information = html_writer::tag('ul', $information);
1109             }
1110             $information = trim($information);
1111         }
1112         return $available;
1113     }
1115     /**
1116      * Shows a time either as a date or a full date and time, according to
1117      * user's timezone.
1118      *
1119      * @param int $time Time
1120      * @param bool $dateonly If true, uses date only
1121      * @return string Date
1122      */
1123     private function show_time($time, $dateonly) {
1124         return userdate($time,
1125                 get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
1126     }
1128     /**
1129      * Checks whether availability information should be shown to normal users.
1130      *
1131      * @return bool True if information about availability should be shown to
1132      *   normal users
1133      * @throws coding_exception If data wasn't loaded
1134      */
1135     public function show_availability() {
1136         $this->require_data();
1137         return $this->item->showavailability;
1138     }
1140     /**
1141      * Internal function cheks that data was loaded.
1142      *
1143      * @throws coding_exception If data wasn't loaded
1144      */
1145     private function require_data() {
1146         if (!$this->gotdata) {
1147             throw new coding_exception('Error: cannot call when info was ' .
1148                 'constructed without data');
1149         }
1150     }
1152     /**
1153      * Obtains a grade score. Note that this score should not be displayed to
1154      * the user, because gradebook rules might prohibit that. It may be a
1155      * non-final score subject to adjustment later.
1156      *
1157      * @global stdClass $USER
1158      * @global moodle_database $DB
1159      * @global stdClass $SESSION
1160      * @param int $gradeitemid Grade item ID we're interested in
1161      * @param bool $grabthelot If true, grabs all scores for current user on
1162      *   this course, so that later ones come from cache
1163      * @param int $userid Set if requesting grade for a different user (does
1164      *   not use cache)
1165      * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
1166      *   or 37.21), or false if user does not have a grade yet
1167      */
1168     private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
1169         global $USER, $DB, $SESSION;
1170         if ($userid==0 || $userid==$USER->id) {
1171             // For current user, go via cache in session
1172             if (empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) {
1173                 $SESSION->gradescorecache = array();
1174                 $SESSION->gradescorecacheuserid = $USER->id;
1175             }
1176             if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
1177                 if ($grabthelot) {
1178                     // Get all grades for the current course
1179                     $rs = $DB->get_recordset_sql('
1180                             SELECT
1181                                 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
1182                             FROM
1183                                 {grade_items} gi
1184                                 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
1185                             WHERE
1186                                 gi.courseid = ?', array($USER->id, $this->item->course));
1187                     foreach ($rs as $record) {
1188                         $SESSION->gradescorecache[$record->id] =
1189                             is_null($record->finalgrade)
1190                                 // No grade = false
1191                                 ? false
1192                                 // Otherwise convert grade to percentage
1193                                 : (($record->finalgrade - $record->rawgrademin) * 100) /
1194                                     ($record->rawgrademax - $record->rawgrademin);
1196                     }
1197                     $rs->close();
1198                     // And if it's still not set, well it doesn't exist (eg
1199                     // maybe the user set it as a condition, then deleted the
1200                     // grade item) so we call it false
1201                     if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
1202                         $SESSION->gradescorecache[$gradeitemid] = false;
1203                     }
1204                 } else {
1205                     // Just get current grade
1206                     $record = $DB->get_record('grade_grades', array(
1207                         'userid'=>$USER->id, 'itemid'=>$gradeitemid));
1208                     if ($record && !is_null($record->finalgrade)) {
1209                         $score = (($record->finalgrade - $record->rawgrademin) * 100) /
1210                             ($record->rawgrademax - $record->rawgrademin);
1211                     } else {
1212                         // Treat the case where row exists but is null, same as
1213                         // case where row doesn't exist
1214                         $score = false;
1215                     }
1216                     $SESSION->gradescorecache[$gradeitemid]=$score;
1217                 }
1218             }
1219             return $SESSION->gradescorecache[$gradeitemid];
1220         } else {
1221             // Not the current user, so request the score individually
1222             $record = $DB->get_record('grade_grades', array(
1223                 'userid'=>$userid, 'itemid'=>$gradeitemid));
1224             if ($record && !is_null($record->finalgrade)) {
1225                 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
1226                     ($record->rawgrademax - $record->rawgrademin);
1227             } else {
1228                 // Treat the case where row exists but is null, same as
1229                 // case where row doesn't exist
1230                 $score = false;
1231             }
1232             return $score;
1233         }
1234     }
1236     /**
1237      * Returns true if a field meets the required conditions, false otherwise.
1238      *
1239      * @param string $operator the requirement/condition
1240      * @param string $uservalue the user's value
1241      * @param string $value the value required
1242      * @return boolean
1243      */
1244     private function is_field_condition_met($operator, $uservalue, $value) {
1245         if ($uservalue === false) {
1246             // If the user value is false this is an instant fail.
1247             // All user values come from the database as either data or the default.
1248             // They will always be a string.
1249             return false;
1250         }
1251         $fieldconditionmet = true;
1252         // Just to be doubly sure it is a string.
1253         $uservalue = (string)$uservalue;
1254         switch($operator) {
1255             case OP_CONTAINS: // contains
1256                 $pos = strpos($uservalue, $value);
1257                 if ($pos === false) {
1258                     $fieldconditionmet = false;
1259                 }
1260                 break;
1261             case OP_DOES_NOT_CONTAIN: // does not contain
1262                 if (!empty($value)) {
1263                     $pos = strpos($uservalue, $value);
1264                     if ($pos !== false) {
1265                         $fieldconditionmet = false;
1266                     }
1267                 }
1268                 break;
1269             case OP_IS_EQUAL_TO: // equal to
1270                 if ($value !== $uservalue) {
1271                     $fieldconditionmet = false;
1272                 }
1273                 break;
1274             case OP_STARTS_WITH: // starts with
1275                 $length = strlen($value);
1276                 if ((substr($uservalue, 0, $length) !== $value)) {
1277                     $fieldconditionmet = false;
1278                 }
1279                 break;
1280             case OP_ENDS_WITH: // ends with
1281                 $length = strlen($value);
1282                 $start  = $length * -1; // negative
1283                 if (substr($uservalue, $start) !== $value) {
1284                     $fieldconditionmet = false;
1285                 }
1286                 break;
1287             case OP_IS_EMPTY: // is empty
1288                 if (!empty($uservalue)) {
1289                     $fieldconditionmet = false;
1290                 }
1291                 break;
1292             case OP_IS_NOT_EMPTY: // is not empty
1293                 if (empty($uservalue)) {
1294                     $fieldconditionmet = false;
1295                 }
1296                 break;
1297         }
1298         return $fieldconditionmet;
1299     }
1301     /**
1302      * Return the value for a user's profile field
1303      *
1304      * @param int $userid set if requesting grade for a different user (does not use cache)
1305      * @param int $fieldid the user profile field id
1306      * @return string the user value, or false if user does not have a user field value yet
1307      */
1308     protected function get_cached_user_profile_field($userid, $fieldid) {
1309         global $USER, $DB, $CFG;
1311         if ($userid === 0) {
1312             // Map out userid = 0 to the current user
1313             $userid = $USER->id;
1314         }
1315         $iscurrentuser = $USER->id == $userid;
1317         if (isguestuser($userid)) {
1318             // Must be logged in and can't be the guest. (this should never happen anyway)
1319             return false;
1320         }
1322         // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
1323         $iscustomprofilefield = is_numeric($fieldid);
1324         if ($iscustomprofilefield) {
1325             // As its a custom profile field we need to map the id back to the actual field.
1326             // We'll also preload all of the other custom profile fields just in case and ensure we have the
1327             // default value available as well.
1328             if ($this->customprofilefields === null) {
1329                 $this->customprofilefields = $DB->get_records('user_info_field', null, 'sortorder ASC, id ASC', 'id, shortname, defaultdata');
1330             }
1331             if (!array_key_exists($fieldid, $this->customprofilefields)) {
1332                 // No such field exists.
1333                 // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
1334                 // or when restoring a backup of a course with user profile field conditions.
1335                 return false;
1336             }
1337             $field = $this->customprofilefields[$fieldid]->shortname;
1338         } else {
1339             $field = $fieldid;
1340         }
1342         // If its the current user than most likely we will be able to get this information from $USER.
1343         // If its a regular profile field then it should already be available, if not then we have a mega problem.
1344         // If its a custom profile field then it should be available but may not be. If it is then we use the value
1345         // available, otherwise we load all custom profile fields into a temp object and refer to that.
1346         // Noting its not going be great for performance if we have to use the temp object as it involves loading the
1347         // custom profile field API and classes.
1348         if ($iscurrentuser) {
1349             if (!$iscustomprofilefield) {
1350                 if (property_exists($USER, $field)) {
1351                     return $USER->{$field};
1352                 } else {
1353                     // Unknown user field. This should not happen.
1354                     throw new coding_exception('Requested user profile field does not exist');
1355                 }
1356             }
1357             // Checking if the custom profile fields are already available.
1358             if (!isset($USER->profile)) {
1359                 // Drat! they're not. We need to use a temp object and load them.
1360                 // We don't use $USER as the profile fields are loaded into the object.
1361                 $user = new stdClass;
1362                 $user->id = $USER->id;
1363                 // This should ALWAYS be set, but just in case we check.
1364                 require_once($CFG->dirroot.'/user/profile/lib.php');
1365                 profile_load_custom_fields($user);
1366                 if (array_key_exists($field, $user->profile)) {
1367                     return $user->profile[$field];
1368                 }
1369             } else if (array_key_exists($field, $USER->profile)) {
1370                 // Hurrah they're available, this is easy.
1371                 return $USER->profile[$field];
1372             }
1373             // The profile field doesn't exist.
1374             return false;
1375         } else {
1376             // Loading for another user.
1377             if ($iscustomprofilefield) {
1378                 // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
1379                 // for us (this will likely be hit again).
1380                 // We are able to do this because we've already pre-loaded the custom fields.
1381                 $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 'fieldid' => $fieldid), IGNORE_MISSING);
1382                 // If we have data return that, otherwise return the default.
1383                 if ($data !== false) {
1384                     return $data;
1385                 } else {
1386                     return $this->customprofilefields[$field]->defaultdata;
1387                 }
1388             } else {
1389                 // Its a standard field, retrieve it from the user.
1390                 return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
1391             }
1392         }
1393         return false;
1394     }
1396     /**
1397      * For testing only. Wipes information cached in user session.
1398      *
1399      * @global stdClass $SESSION
1400      */
1401     static function wipe_session_cache() {
1402         global $SESSION;
1403         unset($SESSION->gradescorecache);
1404         unset($SESSION->gradescorecacheuserid);
1405         unset($SESSION->userfieldcache);
1406         unset($SESSION->userfieldcacheuserid);
1407     }
1409     /**
1410      * Initialises the global cache
1411      * @global stdClass $CONDITIONLIB_PRIVATE
1412      */
1413     public static function init_global_cache() {
1414         global $CONDITIONLIB_PRIVATE;
1415         $CONDITIONLIB_PRIVATE = new stdClass;
1416         $CONDITIONLIB_PRIVATE->usedincondition = array();
1417         $CONDITIONLIB_PRIVATE->groupingscache = array();
1418     }
1420     /**
1421      * Utility function that resets grade/completion conditions in table based
1422      * in data from editing form.
1423      *
1424      * @param condition_info_base $ci Condition info
1425      * @param object $fromform Data from form
1426      * @param bool $wipefirst If true, wipes existing conditions
1427      */
1428     protected static function update_from_form(condition_info_base $ci, $fromform, $wipefirst) {
1429         if ($wipefirst) {
1430             $ci->wipe_conditions();
1431         }
1432         foreach ($fromform->conditiongradegroup as $record) {
1433             if($record['conditiongradeitemid']) {
1434                 $ci->add_grade_condition($record['conditiongradeitemid'],
1435                     unformat_float($record['conditiongrademin']), unformat_float($record['conditiongrademax']));
1436             }
1437         }
1438         foreach ($fromform->conditionfieldgroup as $record) {
1439             if($record['conditionfield']) {
1440                 $ci->add_user_field_condition($record['conditionfield'],
1441                         $record['conditionfieldoperator'],
1442                         $record['conditionfieldvalue']);
1443             }
1444         }
1445         if(isset ($fromform->conditioncompletiongroup)) {
1446             foreach($fromform->conditioncompletiongroup as $record) {
1447                 if($record['conditionsourcecmid']) {
1448                     $ci->add_completion_condition($record['conditionsourcecmid'],
1449                         $record['conditionrequiredcompletion']);
1450                 }
1451             }
1452         }
1453     }
1456 condition_info::init_global_cache();