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