Merge branch 'MDL-38035-master' of git://github.com/sammarshallou/moodle
[moodle.git] / lib / conditionlib.php
CommitLineData
82bd6a5e 1<?php
117bd748
PS
2// This file is part of Moodle - http://moodle.org/
3//
3564771d 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.
117bd748 13//
3564771d 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/>.
16
17/**
18 * Used for tracking conditions that apply before activities are displayed
19 * to students ('conditional availability').
117bd748 20 *
9ccadad3
AKA
21 * @package core_condition
22 * @category condition
78bfb562
PS
23 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3564771d 25 */
82bd6a5e 26
78bfb562
PS
27defined('MOODLE_INTERNAL') || die();
28
47ee3c55
EL
29/**
30 * CONDITION_STUDENTVIEW_HIDE - The activity is not displayed to students at all when conditions aren't met.
9ccadad3 31 */
ce4dfd27 32define('CONDITION_STUDENTVIEW_HIDE', 0);
47ee3c55
EL
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.
9ccadad3 36 */
ce4dfd27 37define('CONDITION_STUDENTVIEW_SHOW', 1);
82bd6a5e 38
47ee3c55 39/**
ce4dfd27 40 * CONDITION_MISSING_NOTHING - The $item variable is expected to contain all completion-related data
9ccadad3 41 */
ce4dfd27 42define('CONDITION_MISSING_NOTHING', 0);
47ee3c55 43/**
ce4dfd27 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
9ccadad3 46 */
ce4dfd27 47define('CONDITION_MISSING_EXTRATABLE', 1);
47ee3c55 48/**
ce4dfd27 49 * CONDITION_MISSING_EVERYTHING - The $item variable is expected to contain nothing except the ID
9ccadad3 50 */
ce4dfd27 51define('CONDITION_MISSING_EVERYTHING', 2);
82bd6a5e 52
cb4492c2
MN
53/**
54 * OP_CONTAINS - comparison operator that determines whether a specified user field contains
55 * a provided variable
56 */
e7b4bd3e 57define('OP_CONTAINS', 'contains');
cb4492c2
MN
58/**
59 * OP_DOES_NOT_CONTAIN - comparison operator that determines whether a specified user field does not
60 * contain a provided variable
61 */
e7b4bd3e 62define('OP_DOES_NOT_CONTAIN', 'doesnotcontain');
cb4492c2
MN
63/**
64 * OP_IS_EQUAL_TO - comparison operator that determines whether a specified user field is equal to
65 * a provided variable
66 */
e7b4bd3e 67define('OP_IS_EQUAL_TO', 'isequalto');
cb4492c2
MN
68/**
69 * OP_STARTS_WITH - comparison operator that determines whether a specified user field starts with
70 * a provided variable
71 */
e7b4bd3e 72define('OP_STARTS_WITH', 'startswith');
cb4492c2
MN
73/**
74 * OP_ENDS_WITH - comparison operator that determines whether a specified user field ends with
75 * a provided variable
76 */
e7b4bd3e 77define('OP_ENDS_WITH', 'endswith');
cb4492c2
MN
78/**
79 * OP_IS_EMPTY - comparison operator that determines whether a specified user field is empty
80 */
e7b4bd3e 81define('OP_IS_EMPTY', 'isempty');
21a4e24c
SH
82/**
83 * OP_IS_NOT_EMPTY - comparison operator that determines whether a specified user field is not empty
84 */
85define('OP_IS_NOT_EMPTY', 'isnotempty');
cb4492c2 86
516c5eca
PS
87require_once($CFG->libdir.'/completionlib.php');
88
3564771d 89/**
9ccadad3 90 * Core class to handle conditional activites
47ee3c55 91 *
9ccadad3
AKA
92 * @package core_condition
93 * @category condition
ce4dfd27 94 * @copyright 2012 The Open University
9ccadad3 95 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3564771d 96 */
ce4dfd27 97class condition_info extends condition_info_base {
82bd6a5e 98 /**
99 * Constructs with course-module details.
100 *
9ccadad3 101 * @global moodle_database $DB
3564771d 102 * @uses CONDITION_MISSING_NOTHING
82bd6a5e 103 * @param object $cm Moodle course-module object. May have extra fields
117bd748
PS
104 * ->conditionsgrade, ->conditionscompletion which should come from
105 * get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
540f89d7 106 * and ->showavailability, ->course, ->visible; but the only required
107 * thing is ->id.
82bd6a5e 108 * @param int $expectingmissing Used to control whether or not a developer
117bd748
PS
109 * debugging message (performance warning) will be displayed if some of
110 * the above data is missing and needs to be retrieved; a
82bd6a5e 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
ce4dfd27 114 */
115 public function __construct($cm, $expectingmissing = CONDITION_MISSING_NOTHING,
116 $loaddata=true) {
117 parent::__construct($cm, 'course_modules', 'coursemoduleid',
118 $expectingmissing, $loaddata);
119 }
120
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 }
133
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
94dc3c7d 138 */
ce4dfd27 139 public function get_full_course_module() {
140 return $this->get_full_item();
141 }
142
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 }
155
94dc3c7d 156 /**
ce4dfd27 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();
173
174 // Activities
175 foreach ($modinfo->cms as $othercm) {
94dc3c7d 176 foreach ($othercm->conditionscompletion as $cmid => $expectedcompletion) {
ce4dfd27 177 $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
178 }
179 }
180
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 }
acfee0d4 190
191 protected function get_context() {
192 return context_module::instance($this->item->id);
193 }
ce4dfd27 194}
195
196
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 */
205class 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 }
227
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 }
240
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
94dc3c7d 245 */
ce4dfd27 246 public function get_full_section() {
247 return $this->get_full_item();
248 }
249
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 }
257
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;
283
284 $available = parent::is_available($information, $grabthelot, $userid, $modinfo);
285
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 }
acfee0d4 292 $context = $this->get_context();
ce4dfd27 293
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;
b93e0af3 310 $information .= get_string('groupingnoaccess', 'condition');
ce4dfd27 311 }
312 } else {
313 // Request is for current user - use cache
94dc3c7d 314 if( !array_key_exists($this->item->course, $CONDITIONLIB_PRIVATE->groupingscache)) {
ce4dfd27 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 }
335
336 $usergroupings = $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course];
337 if ($usergroupings !== true && !array_key_exists($this->item->groupingid, $usergroupings)) {
338 $available = false;
b93e0af3 339 $information .= get_string('groupingnoaccess', 'condition');
ce4dfd27 340 }
341 }
342 }
343
344 $information = trim($information);
345 return $available;
346 }
347
348 /**
94dc3c7d
EL
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 */
ce4dfd27 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 }
acfee0d4 360
361 protected function get_context() {
362 return context_course::instance($this->item->course);
363 }
ce4dfd27 364}
365
366
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 */
377abstract class condition_info_base {
141d3c86
SH
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;
2ede466e
SH
389 /** @var array An array of custom profile field ids => to their shortname */
390 protected $customprofilefields = null;
ce4dfd27 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
82bd6a5e 414 * @return condition_info Object which can retrieve information about the
415 * activity
416 */
ce4dfd27 417 public function __construct($item, $tableprefix, $idfield, $expectingmissing, $loaddata) {
82bd6a5e 418 global $DB;
419
420 // Check ID as otherwise we can't do the other queries
ce4dfd27 421 if (empty($item->id)) {
422 throw new coding_exception('Invalid parameters; item ID not included');
82bd6a5e 423 }
424
94dc3c7d 425 // DB table to store availability conditions
ce4dfd27 426 $this->availtable = $tableprefix . '_availability';
427
e01fbcf7
MN
428 // DB table to store availability conditions for user fields
429 $this->availfieldtable = $tableprefix . '_avail_fields';
430
94dc3c7d 431 // Name of module/section ID field in DB
ce4dfd27 432 $this->idfieldname = $idfield;
433
82bd6a5e 434 // If not loading data, don't do anything else
373fb9dd 435 if (!$loaddata) {
ce4dfd27 436 $this->item = (object)array('id' => $item->id);
373fb9dd 437 $this->gotdata = false;
82bd6a5e 438 return;
439 }
440
441 // Missing basic data from course_modules
ce4dfd27 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) {
373fb9dd 451 if ($expectingmissing<CONDITION_MISSING_EVERYTHING) {
ce4dfd27 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);
82bd6a5e 457 }
ce4dfd27 458 $item = $DB->get_record($tableprefix, array('id' => $item->id),
459 implode(',', $basicfields), MUST_EXIST);
82bd6a5e 460 }
461
ce4dfd27 462 $this->item = clone($item);
373fb9dd 463 $this->gotdata = true;
82bd6a5e 464
465 // Missing extra data
141d3c86 466 if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion) || !isset($item->conditionsfield)) {
373fb9dd 467 if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) {
ce4dfd27 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);
82bd6a5e 473 }
474
ce4dfd27 475 $this->fill_availability_conditions($this->item);
82bd6a5e 476 }
477 }
478
479 /**
ce4dfd27 480 * Gets list of required fields from main table.
82bd6a5e 481 *
ce4dfd27 482 * @return array Array of field names
483 */
484 protected function get_main_table_fields() {
540f89d7 485 return array('id', 'course', 'visible',
486 'availablefrom', 'availableuntil', 'showavailability');
ce4dfd27 487 }
488
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
82bd6a5e 496 */
ce4dfd27 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');
82bd6a5e 501 }
502
503 // Does nothing if the variables are already present
141d3c86 504 if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion) || !isset($item->conditionsfield)) {
ce4dfd27 505 $item->conditionsgrade = array();
506 $item->conditionscompletion = array();
76af15bb 507 $item->conditionsfield = array();
ce4dfd27 508
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));
373fb9dd 517 foreach ($conditions as $condition) {
518 if (!is_null($condition->sourcecmid)) {
ce4dfd27 519 $item->conditionscompletion[$condition->sourcecmid] =
82bd6a5e 520 $condition->requiredcompletion;
373fb9dd 521 } else {
522 $minmax = new stdClass;
523 $minmax->min = $condition->conditiongrademin;
524 $minmax->max = $condition->conditiongrademax;
525 $minmax->name = self::get_grade_name($condition);
ce4dfd27 526 $item->conditionsgrade[$condition->gradeitemid] = $minmax;
82bd6a5e 527 }
528 }
e01fbcf7 529
76af15bb 530 // For user fields
e01fbcf7
MN
531 $sql = "SELECT a.id as cmaid, a.*, uf.*
532 FROM {" . $tableprefix . "_avail_fields} a
141d3c86 533 LEFT JOIN {user_info_field} uf ON a.customfieldid = uf.id
e01fbcf7 534 WHERE " . $idfield . " = :itemid";
141d3c86
SH
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;
76af15bb 545 } else {
141d3c86 546 $fieldname = '!missing';
76af15bb 547 }
141d3c86
SH
548 } else {
549 $field = $condition->userfield;
550 $fieldname = $condition->userfield;
76af15bb 551 }
141d3c86
SH
552 $details = new stdClass;
553 $details->fieldname = $fieldname;
554 $details->operator = $condition->operator;
555 $details->value = $condition->value;
556 $item->conditionsfield[$field] = $details;
76af15bb 557 }
82bd6a5e 558 }
559 }
373fb9dd 560
82bd6a5e 561 /**
562 * Obtains the name of a grade item.
3564771d 563 *
ce4dfd27 564 * @global object $CFG
82bd6a5e 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;
373fb9dd 571 if (isset($gradeitemobj->id)) {
ce4dfd27 572 require_once($CFG->libdir . '/gradelib.php');
373fb9dd 573 $item = new grade_item;
117bd748 574 grade_object::set_properties($item, $gradeitemobj);
82bd6a5e 575 return $item->get_name();
576 } else {
577 return '!missing'; // Ooops, missing grade
578 }
579 }
580
581 /**
ce4dfd27 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
82bd6a5e 585 */
ce4dfd27 586 protected function get_full_item() {
82bd6a5e 587 $this->require_data();
ce4dfd27 588 return $this->item;
82bd6a5e 589 }
590
76af15bb
MN
591 /**
592 * The operators that provide the relationship
593 * between a field and a value.
594 *
cb4492c2 595 * @return array Associative array from operator constant to display name
76af15bb 596 */
cb4492c2 597 public static function get_condition_user_field_operators() {
76af15bb 598 return array(
21a4e24c
SH
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'),
cb4492c2 606 );
76af15bb
MN
607 }
608
609 /**
3a09f4dc 610 * Returns list of user fields that can be compared.
76af15bb 611 *
3a09f4dc 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
cb4492c2 618 * @return array Associative array from user field constants to display name
76af15bb 619 */
3a09f4dc 620 public static function get_condition_user_fields($formatoptions = null) {
76af15bb
MN
621 global $DB;
622
623 $userfields = array(
cb4492c2
MN
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'),
cb4492c2
MN
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'),
e7b4bd3e 638 'phone1' => get_user_field_name('phone1'),
cb4492c2
MN
639 'phone2' => get_user_field_name('phone2'),
640 'address' => get_user_field_name('address')
641 );
76af15bb
MN
642
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) {
3a09f4dc 646 if ($formatoptions) {
647 $userfields[$field->id] = format_string($field->name, true, $formatoptions);
648 } else {
649 $userfields[$field->id] = $field->name;
650 }
76af15bb
MN
651 }
652 }
653
654 return $userfields;
655 }
656
82bd6a5e 657 /**
658 * Adds to the database a condition based on completion of another module.
3564771d 659 *
9ccadad3 660 * @global moodle_database $DB
82bd6a5e 661 * @param int $cmid ID of other module
662 * @param int $requiredcompletion COMPLETION_xx constant
663 */
373fb9dd 664 public function add_completion_condition($cmid, $requiredcompletion) {
82bd6a5e 665 global $DB;
ce4dfd27 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);
82bd6a5e 671
672 // Store in memory too
ce4dfd27 673 $this->item->conditionscompletion[$cmid] = $requiredcompletion;
82bd6a5e 674 }
675
76af15bb
MN
676 /**
677 * Adds user fields condition
678 *
76af15bb
MN
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) {
76af15bb 685 global $DB;
cb4492c2 686
e01fbcf7
MN
687 // Get the field name
688 $idfieldname = $this->idfieldname;
689
cb4492c2 690 $objavailfield = new stdClass;
e01fbcf7 691 $objavailfield->$idfieldname = $this->item->id;
cb4492c2 692 if (is_numeric($field)) { // If the condition field is numeric then it is a custom profile field
129e4a73
MN
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;
cb4492c2
MN
696 $objavailfield->customfieldid = $field;
697 } else {
129e4a73 698 $objavailfield->fieldname = $field;
cb4492c2
MN
699 $objavailfield->userfield = $field;
700 }
701 $objavailfield->operator = $operator;
702 $objavailfield->value = $value;
e01fbcf7 703 $DB->insert_record($this->availfieldtable, $objavailfield, false);
76af15bb
MN
704
705 // Store in memory too
e01fbcf7 706 $this->item->conditionsfield[$field] = $objavailfield;
76af15bb
MN
707 }
708
82bd6a5e 709 /**
710 * Adds to the database a condition based on the value of a grade item.
3564771d 711 *
9ccadad3 712 * @global moodle_database $DB
82bd6a5e 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 */
373fb9dd 720 public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
ce4dfd27 721 global $DB;
82bd6a5e 722 // Normalise nulls
373fb9dd 723 if ($min==='') {
724 $min = null;
82bd6a5e 725 }
373fb9dd 726 if ($max==='') {
727 $max = null;
82bd6a5e 728 }
729 // Add to DB
ce4dfd27 730 $DB->insert_record($this->availtable, (object)array(
731 $this->idfieldname => $this->item->id,
732 'gradeitemid' => $gradeitemid, 'grademin' => $min, 'grademax' => $max),
733 false);
82bd6a5e 734
735 // Store in memory too
373fb9dd 736 if ($updateinmemory) {
ce4dfd27 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)));
82bd6a5e 741 }
742 }
743
ce4dfd27 744 /**
82bd6a5e 745 * Erases from the database all conditions for this activity.
3564771d 746 *
9ccadad3 747 * @global moodle_database $DB
82bd6a5e 748 */
749 public function wipe_conditions() {
750 // Wipe from DB
751 global $DB;
76af15bb 752
ce4dfd27 753 $DB->delete_records($this->availtable, array($this->idfieldname => $this->item->id));
e01fbcf7 754 $DB->delete_records($this->availfieldtable, array($this->idfieldname => $this->item->id));
82bd6a5e 755
756 // And from memory
ce4dfd27 757 $this->item->conditionsgrade = array();
758 $this->item->conditionscompletion = array();
76af15bb 759 $this->item->conditionsfield = array();
82bd6a5e 760 }
761
762 /**
763 * Obtains a string describing all availability restrictions (even if
764 * they do not apply any more).
3564771d 765 *
9ccadad3
AKA
766 * @global stdClass $COURSE
767 * @global moodle_database $DB
82bd6a5e 768 * @param object $modinfo Usually leave as null for default. Specify when
117bd748 769 * calling recursively from inside get_fast_modinfo. The value supplied
82bd6a5e 770 * here must include list of all CMs with 'id' and 'name'
117bd748 771 * @return string Information string (for admin) about all restrictions on
82bd6a5e 772 * this item
82bd6a5e 773 */
774 public function get_full_information($modinfo=null) {
373fb9dd 775 global $COURSE, $DB;
ce4dfd27 776 $this->require_data();
82bd6a5e 777
373fb9dd 778 $information = '';
82bd6a5e 779
9a8caba2 780
82bd6a5e 781 // Completion conditions
ce4dfd27 782 if (count($this->item->conditionscompletion) > 0) {
783 if ($this->item->course == $COURSE->id) {
373fb9dd 784 $course = $COURSE;
82bd6a5e 785 } else {
ce4dfd27 786 $course = $DB->get_record('course', array('id' => $this->item->course),
2b64e24e 787 'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
82bd6a5e 788 }
ce4dfd27 789 foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
373fb9dd 790 if (!$modinfo) {
791 $modinfo = get_fast_modinfo($course);
82bd6a5e 792 }
bfeaa895
SM
793 if (empty($modinfo->cms[$cmid])) {
794 continue;
795 }
9a8caba2 796 $information .= html_writer::start_tag('li');
b93e0af3
DP
797 $information .= get_string(
798 'requires_completion_' . $expectedcompletion,
799 'condition', $modinfo->cms[$cmid]->name) . ' ';
9a8caba2 800 $information .= html_writer::end_tag('li');
82bd6a5e 801 }
802 }
803
804 // Grade conditions
ce4dfd27 805 if (count($this->item->conditionsgrade) > 0) {
806 foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
82bd6a5e 807 // String depends on type of requirement. We are coy about
808 // the actual numbers, in case grades aren't released to
809 // students.
373fb9dd 810 if (is_null($minmax->min) && is_null($minmax->max)) {
811 $string = 'any';
812 } else if (is_null($minmax->max)) {
813 $string = 'min';
814 } else if (is_null($minmax->min)) {
815 $string = 'max';
82bd6a5e 816 } else {
373fb9dd 817 $string = 'range';
82bd6a5e 818 }
9a8caba2 819 $information .= html_writer::start_tag('li');
b93e0af3 820 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
9a8caba2 821 $information .= html_writer::end_tag('li');
82bd6a5e 822 }
823 }
824
76af15bb 825 // User field conditions
e01fbcf7 826 if (count($this->item->conditionsfield) > 0) {
acfee0d4 827 $context = $this->get_context();
76af15bb 828 // Need the array of operators
e01fbcf7 829 foreach ($this->item->conditionsfield as $field => $details) {
76af15bb 830 $a = new stdclass;
acfee0d4 831 $a->field = format_string($details->fieldname, true, array('context' => $context));
76af15bb 832 $a->value = $details->value;
9a8caba2 833 $information .= html_writer::start_tag('li');
1c8d0933 834 $information .= get_string('requires_user_field_'.$details->operator, 'condition', $a) . ' ';
9a8caba2 835 $information .= html_writer::end_tag('li');
76af15bb
MN
836 }
837 }
838
6282381d 839 // The date logic is complicated. The intention of this logic is:
840 // 1) display date without time where possible (whenever the date is
841 // midnight)
842 // 2) when the 'until' date is e.g. 00:00 on the 14th, we display it as
843 // 'until the 13th' (experience at the OU showed that students are
844 // likely to interpret 'until <date>' as 'until the end of <date>').
845 // 3) This behaviour becomes confusing for 'same-day' dates where there
846 // are some exceptions.
847 // Users in different time zones will typically not get the 'abbreviated'
848 // behaviour but it should work OK for them aside from that.
849
850 // The following cases are possible:
851 // a) From 13:05 on 14 Oct until 12:10 on 17 Oct (exact, exact)
852 // b) From 14 Oct until 12:11 on 17 Oct (midnight, exact)
853 // c) From 13:05 on 14 Oct until 17 Oct (exact, midnight 18 Oct)
854 // d) From 14 Oct until 17 Oct (midnight 14 Oct, midnight 18 Oct)
855 // e) On 14 Oct (midnight 14 Oct, midnight 15 Oct)
856 // f) From 13:05 on 14 Oct until 0:00 on 15 Oct (exact, midnight, same day)
857 // g) From 0:00 on 14 Oct until 12:05 on 14 Oct (midnight, exact, same day)
858 // h) From 13:05 on 14 Oct (exact)
859 // i) From 14 Oct (midnight)
860 // j) Until 13:05 on 14 Oct (exact)
861 // k) Until 14 Oct (midnight 15 Oct)
862
863 // Check if start and end dates are 'midnights', if so we show in short form
ce4dfd27 864 $shortfrom = self::is_midnight($this->item->availablefrom);
865 $shortuntil = self::is_midnight($this->item->availableuntil);
6282381d 866
867 // For some checks and for display, we need the previous day for the 'until'
868 // value, if we are going to display it in short form
ce4dfd27 869 if ($this->item->availableuntil) {
870 $daybeforeuntil = strtotime('-1 day', usergetmidnight($this->item->availableuntil));
6282381d 871 }
872
873 // Special case for if one but not both are exact and they are within a day
ce4dfd27 874 if ($this->item->availablefrom && $this->item->availableuntil &&
875 $shortfrom != $shortuntil && $daybeforeuntil < $this->item->availablefrom) {
6282381d 876 // Don't use abbreviated version (see examples f, g above)
877 $shortfrom = false;
878 $shortuntil = false;
879 }
880
881 // When showing short end date, the display time is the 'day before' one
ce4dfd27 882 $displayuntil = $shortuntil ? $daybeforeuntil : $this->item->availableuntil;
6282381d 883
ce4dfd27 884 if ($this->item->availablefrom && $this->item->availableuntil) {
885 if ($shortfrom && $shortuntil && $daybeforeuntil == $this->item->availablefrom) {
9a8caba2 886 $information .= html_writer::start_tag('li');
b93e0af3
DP
887 $information .= get_string('requires_date_both_single_day', 'condition',
888 self::show_time($this->item->availablefrom, true));
9a8caba2 889 $information .= html_writer::end_tag('li');
6282381d 890 } else {
9a8caba2 891 $information .= html_writer::start_tag('li');
b93e0af3
DP
892 $information .= get_string('requires_date_both', 'condition', (object)array(
893 'from' => self::show_time($this->item->availablefrom, $shortfrom),
894 'until' => self::show_time($displayuntil, $shortuntil)));
9a8caba2 895 $information .= html_writer::end_tag('li');
6282381d 896 }
ce4dfd27 897 } else if ($this->item->availablefrom) {
9a8caba2 898 $information .= html_writer::start_tag('li');
b93e0af3
DP
899 $information .= get_string('requires_date', 'condition',
900 self::show_time($this->item->availablefrom, $shortfrom));
9a8caba2 901 $information .= html_writer::end_tag('li');
ce4dfd27 902 } else if ($this->item->availableuntil) {
9a8caba2 903 $information .= html_writer::start_tag('li');
b93e0af3
DP
904 $information .= get_string('requires_date_before', 'condition',
905 self::show_time($displayuntil, $shortuntil));
9a8caba2 906 $information .= html_writer::end_tag('li');
82bd6a5e 907 }
908
9a8caba2
DW
909 // The information is in <li> tags, but to avoid taking up more space
910 // if there is only a single item, we strip out the list tags so that it
911 // is plain text in that case.
912 if (!empty($information)) {
913 $li = strpos($information, '<li>', 4);
914 if ($li === false) {
915 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
916 } else {
917 $information = html_writer::tag('ul', $information);
918 }
919 $information = trim($information);
920 }
82bd6a5e 921 return $information;
922 }
923
6282381d 924 /**
925 * Checks whether a given time refers exactly to midnight (in current user
926 * timezone).
9ccadad3 927 *
6282381d 928 * @param int $time Time
929 * @return bool True if time refers to midnight, false if it's some other
930 * time or if it is set to zero
931 */
932 private static function is_midnight($time) {
933 return $time && usergetmidnight($time) == $time;
934 }
935
82bd6a5e 936 /**
ce4dfd27 937 * Determines whether this particular item is currently available
117bd748
PS
938 * according to these criteria.
939 *
940 * - This does not include the 'visible' setting (i.e. this might return
82bd6a5e 941 * true even if visible is false); visible is handled independently.
942 * - This does not take account of the viewhiddenactivities capability.
943 * That should apply later.
944 *
9ccadad3
AKA
945 * @global stdClass $COURSE
946 * @global moodle_database $DB
3564771d 947 * @uses COMPLETION_COMPLETE
948 * @uses COMPLETION_COMPLETE_FAIL
949 * @uses COMPLETION_COMPLETE_PASS
950 * @param string $information If the item has availability restrictions,
117bd748 951 * a string that describes the conditions will be stored in this variable;
82bd6a5e 952 * if this variable is set blank, that means don't display anything
117bd748 953 * @param bool $grabthelot Performance hint: if true, caches information
82bd6a5e 954 * required for all course-modules, to make the front page and similar
955 * pages work more quickly (works only for current user)
956 * @param int $userid If set, specifies a different user ID to check availability for
957 * @param object $modinfo Usually leave as null for default. Specify when
117bd748 958 * calling recursively from inside get_fast_modinfo. The value supplied
82bd6a5e 959 * here must include list of all CMs with 'id' and 'name'
960 * @return bool True if this item is available to the user, false otherwise
82bd6a5e 961 */
373fb9dd 962 public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
ce4dfd27 963 global $COURSE, $DB;
373fb9dd 964 $this->require_data();
82bd6a5e 965
373fb9dd 966 $available = true;
967 $information = '';
82bd6a5e 968
969 // Check each completion condition
ce4dfd27 970 if (count($this->item->conditionscompletion) > 0) {
971 if ($this->item->course == $COURSE->id) {
373fb9dd 972 $course = $COURSE;
82bd6a5e 973 } else {
ce4dfd27 974 $course = $DB->get_record('course', array('id' => $this->item->course),
2b64e24e 975 'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
82bd6a5e 976 }
977
373fb9dd 978 $completion = new completion_info($course);
ce4dfd27 979 foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
bfeaa895
SM
980 // If this depends on a deleted module, handle that situation
981 // gracefully.
982 if (!$modinfo) {
983 $modinfo = get_fast_modinfo($course);
984 }
985 if (empty($modinfo->cms[$cmid])) {
c8b3346c
PS
986 global $PAGE;
987 if (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0) {
79e9e604 988 debugging("Warning: activity {$this->item->id} '{$this->item->name}' has condition " .
ce4dfd27 989 "on deleted activity $cmid (to get rid of this message, edit the named activity)");
bfeaa895
SM
990 }
991 continue;
992 }
993
82bd6a5e 994 // The completion system caches its own data
ce4dfd27 995 $completiondata = $completion->get_data((object)array('id' => $cmid),
996 $grabthelot, $userid, $modinfo);
82bd6a5e 997
373fb9dd 998 $thisisok = true;
999 if ($expectedcompletion==COMPLETION_COMPLETE) {
82bd6a5e 1000 // 'Complete' also allows the pass, fail states
373fb9dd 1001 switch ($completiondata->completionstate) {
82bd6a5e 1002 case COMPLETION_COMPLETE:
1003 case COMPLETION_COMPLETE_FAIL:
1004 case COMPLETION_COMPLETE_PASS:
1005 break;
1006 default:
373fb9dd 1007 $thisisok = false;
82bd6a5e 1008 }
1009 } else {
1010 // Other values require exact match
373fb9dd 1011 if ($completiondata->completionstate!=$expectedcompletion) {
1012 $thisisok = false;
82bd6a5e 1013 }
1014 }
373fb9dd 1015 if (!$thisisok) {
1016 $available = false;
9a8caba2 1017 $information .= html_writer::start_tag('li');
b93e0af3
DP
1018 $information .= get_string(
1019 'requires_completion_' . $expectedcompletion,
1020 'condition', $modinfo->cms[$cmid]->name) . ' ';
9a8caba2 1021 $information .= html_writer::end_tag('li');
82bd6a5e 1022 }
1023 }
1024 }
1025
1026 // Check each grade condition
ce4dfd27 1027 if (count($this->item->conditionsgrade)>0) {
1028 foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
373fb9dd 1029 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
1030 if ($score===false ||
ce4dfd27 1031 (!is_null($minmax->min) && $score<$minmax->min) ||
1032 (!is_null($minmax->max) && $score>=$minmax->max)) {
82bd6a5e 1033 // Grade fail
373fb9dd 1034 $available = false;
82bd6a5e 1035 // String depends on type of requirement. We are coy about
1036 // the actual numbers, in case grades aren't released to
1037 // students.
373fb9dd 1038 if (is_null($minmax->min) && is_null($minmax->max)) {
1039 $string = 'any';
1040 } else if (is_null($minmax->max)) {
1041 $string = 'min';
1042 } else if (is_null($minmax->min)) {
1043 $string = 'max';
82bd6a5e 1044 } else {
373fb9dd 1045 $string = 'range';
82bd6a5e 1046 }
9a8caba2 1047 $information .= html_writer::start_tag('li');
b93e0af3 1048 $information .= get_string('requires_grade_' . $string, 'condition', $minmax->name) . ' ';
9a8caba2 1049 $information .= html_writer::end_tag('li');
82bd6a5e 1050 }
1051 }
1052 }
1053
76af15bb 1054 // Check if user field condition
e01fbcf7 1055 if (count($this->item->conditionsfield) > 0) {
acfee0d4 1056 $context = $this->get_context();
e01fbcf7 1057 foreach ($this->item->conditionsfield as $field => $details) {
2ede466e
SH
1058 $uservalue = $this->get_cached_user_profile_field($userid, $field);
1059 if (!$this->is_field_condition_met($details->operator, $uservalue, $details->value)) {
76af15bb
MN
1060 // Set available to false
1061 $available = false;
1062 $a = new stdClass();
acfee0d4 1063 $a->field = format_string($details->fieldname, true, array('context' => $context));
a8f84c28 1064 $a->value = $details->value;
9a8caba2 1065 $information .= html_writer::start_tag('li');
21a4e24c 1066 $information .= get_string('requires_user_field_'.$details->operator, 'condition', $a) . ' ';
9a8caba2 1067 $information .= html_writer::end_tag('li');
76af15bb
MN
1068 }
1069 }
1070 }
1071
82bd6a5e 1072 // Test dates
ce4dfd27 1073 if ($this->item->availablefrom) {
1074 if (time() < $this->item->availablefrom) {
373fb9dd 1075 $available = false;
e7c6bf0e 1076
9a8caba2 1077 $information .= html_writer::start_tag('li');
b93e0af3
DP
1078 $information .= get_string('requires_date', 'condition',
1079 self::show_time($this->item->availablefrom,
1080 self::is_midnight($this->item->availablefrom)));
9a8caba2 1081 $information .= html_writer::end_tag('li');
82bd6a5e 1082 }
1083 }
1084
ce4dfd27 1085 if ($this->item->availableuntil) {
1086 if (time() >= $this->item->availableuntil) {
373fb9dd 1087 $available = false;
82bd6a5e 1088 // But we don't display any information about this case. This is
1089 // because the only reason to set a 'disappear' date is usually
1090 // to get rid of outdated information/clutter in which case there
1091 // is no point in showing it...
1092
1093 // Note it would be nice if we could make it so that the 'until'
1094 // date appears below the item while the item is still accessible,
1095 // unfortunately this is not possible in the current system. Maybe
1096 // later, or if somebody else wants to add it.
1097 }
1098 }
1099
540f89d7 1100 // If the item is marked as 'not visible' then we don't change the available
1101 // flag (visible/available are treated distinctly), but we remove any
1102 // availability info. If the item is hidden with the eye icon, it doesn't
1103 // make sense to show 'Available from <date>' or similar, because even
1104 // when that date arrives it will still not be available unless somebody
1105 // toggles the eye icon.
1106 if (!$this->item->visible) {
1107 $information = '';
1108 }
1109
9a8caba2
DW
1110 // The information is in <li> tags, but to avoid taking up more space
1111 // if there is only a single item, we strip out the list tags so that it
1112 // is plain text in that case.
1113 if (!empty($information)) {
1114 $li = strpos($information, '<li>', 4);
1115 if ($li === false) {
1116 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
1117 } else {
1118 $information = html_writer::tag('ul', $information);
1119 }
1120 $information = trim($information);
1121 }
82bd6a5e 1122 return $available;
1123 }
1124
e7c6bf0e 1125 /**
6282381d 1126 * Shows a time either as a date or a full date and time, according to
1127 * user's timezone.
9ccadad3 1128 *
e7c6bf0e 1129 * @param int $time Time
6282381d 1130 * @param bool $dateonly If true, uses date only
e7c6bf0e 1131 * @return string Date
1132 */
6282381d 1133 private function show_time($time, $dateonly) {
1134 return userdate($time,
1135 get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
e7c6bf0e 1136 }
1137
82bd6a5e 1138 /**
ce4dfd27 1139 * Checks whether availability information should be shown to normal users.
9ccadad3 1140 *
82bd6a5e 1141 * @return bool True if information about availability should be shown to
1142 * normal users
1143 * @throws coding_exception If data wasn't loaded
1144 */
1145 public function show_availability() {
1146 $this->require_data();
ce4dfd27 1147 return $this->item->showavailability;
82bd6a5e 1148 }
117bd748 1149
82bd6a5e 1150 /**
1151 * Internal function cheks that data was loaded.
3564771d 1152 *
ce4dfd27 1153 * @throws coding_exception If data wasn't loaded
82bd6a5e 1154 */
1155 private function require_data() {
373fb9dd 1156 if (!$this->gotdata) {
ce4dfd27 1157 throw new coding_exception('Error: cannot call when info was ' .
82bd6a5e 1158 'constructed without data');
1159 }
1160 }
1161
94dc3c7d 1162 /**
117bd748
PS
1163 * Obtains a grade score. Note that this score should not be displayed to
1164 * the user, because gradebook rules might prohibit that. It may be a
82bd6a5e 1165 * non-final score subject to adjustment later.
1166 *
9ccadad3
AKA
1167 * @global stdClass $USER
1168 * @global moodle_database $DB
1169 * @global stdClass $SESSION
82bd6a5e 1170 * @param int $gradeitemid Grade item ID we're interested in
117bd748 1171 * @param bool $grabthelot If true, grabs all scores for current user on
82bd6a5e 1172 * this course, so that later ones come from cache
117bd748 1173 * @param int $userid Set if requesting grade for a different user (does
75564228 1174 * not use cache)
117bd748 1175 * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
75564228 1176 * or 37.21), or false if user does not have a grade yet
82bd6a5e 1177 */
373fb9dd 1178 private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
82bd6a5e 1179 global $USER, $DB, $SESSION;
fdef13e6 1180 if ($userid==0 || $userid==$USER->id) {
82bd6a5e 1181 // For current user, go via cache in session
373fb9dd 1182 if (empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) {
1183 $SESSION->gradescorecache = array();
1184 $SESSION->gradescorecacheuserid = $USER->id;
117bd748 1185 }
373fb9dd 1186 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
1187 if ($grabthelot) {
82bd6a5e 1188 // Get all grades for the current course
ce4dfd27 1189 $rs = $DB->get_recordset_sql('
1190 SELECT
1191 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
1192 FROM
1193 {grade_items} gi
1194 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
1195 WHERE
1196 gi.courseid = ?', array($USER->id, $this->item->course));
373fb9dd 1197 foreach ($rs as $record) {
1198 $SESSION->gradescorecache[$record->id] =
82bd6a5e 1199 is_null($record->finalgrade)
75564228 1200 // No grade = false
117bd748 1201 ? false
75564228 1202 // Otherwise convert grade to percentage
1203 : (($record->finalgrade - $record->rawgrademin) * 100) /
1204 ($record->rawgrademax - $record->rawgrademin);
82bd6a5e 1205
1206 }
1207 $rs->close();
1208 // And if it's still not set, well it doesn't exist (eg
1209 // maybe the user set it as a condition, then deleted the
1210 // grade item) so we call it false
373fb9dd 1211 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
1212 $SESSION->gradescorecache[$gradeitemid] = false;
82bd6a5e 1213 }
1214 } else {
1215 // Just get current grade
75564228 1216 $record = $DB->get_record('grade_grades', array(
373fb9dd 1217 'userid'=>$USER->id, 'itemid'=>$gradeitemid));
75564228 1218 if ($record && !is_null($record->finalgrade)) {
1219 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
1220 ($record->rawgrademax - $record->rawgrademin);
1221 } else {
1222 // Treat the case where row exists but is null, same as
1223 // case where row doesn't exist
373fb9dd 1224 $score = false;
82bd6a5e 1225 }
1226 $SESSION->gradescorecache[$gradeitemid]=$score;
1227 }
1228 }
1229 return $SESSION->gradescorecache[$gradeitemid];
1230 } else {
1231 // Not the current user, so request the score individually
75564228 1232 $record = $DB->get_record('grade_grades', array(
1233 'userid'=>$userid, 'itemid'=>$gradeitemid));
1234 if ($record && !is_null($record->finalgrade)) {
1235 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
1236 ($record->rawgrademax - $record->rawgrademin);
1237 } else {
1238 // Treat the case where row exists but is null, same as
1239 // case where row doesn't exist
373fb9dd 1240 $score = false;
82bd6a5e 1241 }
1242 return $score;
1243 }
1244 }
1245
cb4492c2
MN
1246 /**
1247 * Returns true if a field meets the required conditions, false otherwise.
1248 *
141d3c86
SH
1249 * @param string $operator the requirement/condition
1250 * @param string $uservalue the user's value
1251 * @param string $value the value required
cb4492c2
MN
1252 * @return boolean
1253 */
a8f84c28 1254 private function is_field_condition_met($operator, $uservalue, $value) {
2ede466e
SH
1255 if ($uservalue === false) {
1256 // If the user value is false this is an instant fail.
1257 // All user values come from the database as either data or the default.
1258 // They will always be a string.
1259 return false;
1260 }
cb4492c2 1261 $fieldconditionmet = true;
2ede466e
SH
1262 // Just to be doubly sure it is a string.
1263 $uservalue = (string)$uservalue;
cb4492c2
MN
1264 switch($operator) {
1265 case OP_CONTAINS: // contains
1266 $pos = strpos($uservalue, $value);
1267 if ($pos === false) {
1268 $fieldconditionmet = false;
1269 }
1270 break;
1271 case OP_DOES_NOT_CONTAIN: // does not contain
1272 if (!empty($value)) {
1273 $pos = strpos($uservalue, $value);
1274 if ($pos !== false) {
1275 $fieldconditionmet = false;
1276 }
1277 }
1278 break;
1279 case OP_IS_EQUAL_TO: // equal to
1280 if ($value !== $uservalue) {
1281 $fieldconditionmet = false;
1282 }
1283 break;
a8f84c28 1284 case OP_STARTS_WITH: // starts with
cb4492c2
MN
1285 $length = strlen($value);
1286 if ((substr($uservalue, 0, $length) !== $value)) {
1287 $fieldconditionmet = false;
1288 }
1289 break;
1290 case OP_ENDS_WITH: // ends with
1291 $length = strlen($value);
a8f84c28 1292 $start = $length * -1; // negative
cb4492c2
MN
1293 if (substr($uservalue, $start) !== $value) {
1294 $fieldconditionmet = false;
1295 }
1296 break;
1297 case OP_IS_EMPTY: // is empty
1298 if (!empty($uservalue)) {
1299 $fieldconditionmet = false;
1300 }
1301 break;
21a4e24c
SH
1302 case OP_IS_NOT_EMPTY: // is not empty
1303 if (empty($uservalue)) {
1304 $fieldconditionmet = false;
1305 }
1306 break;
cb4492c2
MN
1307 }
1308 return $fieldconditionmet;
1309 }
1310
76af15bb
MN
1311 /**
1312 * Return the value for a user's profile field
1313 *
141d3c86 1314 * @param int $userid set if requesting grade for a different user (does not use cache)
76af15bb 1315 * @param int $fieldid the user profile field id
cb4492c2 1316 * @return string the user value, or false if user does not have a user field value yet
76af15bb 1317 */
6fea7a5f 1318 protected function get_cached_user_profile_field($userid, $fieldid) {
2ede466e
SH
1319 global $USER, $DB, $CFG;
1320
1321 if ($userid === 0) {
1322 // Map out userid = 0 to the current user
1323 $userid = $USER->id;
1324 }
1325 $iscurrentuser = $USER->id == $userid;
1326
1327 if (isguestuser($userid)) {
1328 // Must be logged in and can't be the guest. (this should never happen anyway)
1329 return false;
1330 }
1331
1332 // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
1333 $iscustomprofilefield = is_numeric($fieldid);
1334 if ($iscustomprofilefield) {
1335 // As its a custom profile field we need to map the id back to the actual field.
1336 // We'll also preload all of the other custom profile fields just in case and ensure we have the
1337 // default value available as well.
1338 if ($this->customprofilefields === null) {
1339 $this->customprofilefields = $DB->get_records('user_info_field', null, 'sortorder ASC, id ASC', 'id, shortname, defaultdata');
1340 }
1341 if (!array_key_exists($fieldid, $this->customprofilefields)) {
1342 // No such field exists.
1343 // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
1344 // or when restoring a backup of a course with user profile field conditions.
1345 return false;
1346 }
1347 $field = $this->customprofilefields[$fieldid]->shortname;
1348 } else {
1349 $field = $fieldid;
1350 }
1351
1352 // If its the current user than most likely we will be able to get this information from $USER.
1353 // If its a regular profile field then it should already be available, if not then we have a mega problem.
1354 // If its a custom profile field then it should be available but may not be. If it is then we use the value
1355 // available, otherwise we load all custom profile fields into a temp object and refer to that.
1356 // Noting its not going be great for performance if we have to use the temp object as it involves loading the
1357 // custom profile field API and classes.
1358 if ($iscurrentuser) {
1359 if (!$iscustomprofilefield) {
1360 if (property_exists($USER, $field)) {
1361 return $USER->{$field};
cb4492c2 1362 } else {
2ede466e
SH
1363 // Unknown user field. This should not happen.
1364 throw new coding_exception('Requested user profile field does not exist');
cb4492c2 1365 }
76af15bb 1366 }
2ede466e
SH
1367 // Checking if the custom profile fields are already available.
1368 if (!isset($USER->profile)) {
1369 // Drat! they're not. We need to use a temp object and load them.
1370 // We don't use $USER as the profile fields are loaded into the object.
1371 $user = new stdClass;
1372 $user->id = $USER->id;
1373 // This should ALWAYS be set, but just in case we check.
1374 require_once($CFG->dirroot.'/user/profile/lib.php');
1375 profile_load_custom_fields($user);
1376 if (array_key_exists($field, $user->profile)) {
1377 return $user->profile[$field];
1378 }
1379 } else if (array_key_exists($field, $USER->profile)) {
1380 // Hurrah they're available, this is easy.
1381 return $USER->profile[$field];
1382 }
1383 // The profile field doesn't exist.
1384 return false;
76af15bb 1385 } else {
2ede466e 1386 // Loading for another user.
76af15bb 1387 if ($iscustomprofilefield) {
2ede466e
SH
1388 // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
1389 // for us (this will likely be hit again).
1390 // We are able to do this because we've already pre-loaded the custom fields.
1391 $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 'fieldid' => $fieldid), IGNORE_MISSING);
1392 // If we have data return that, otherwise return the default.
1393 if ($data !== false) {
1394 return $data;
1395 } else {
1396 return $this->customprofilefields[$field]->defaultdata;
76af15bb 1397 }
cb4492c2 1398 } else {
2ede466e
SH
1399 // Its a standard field, retrieve it from the user.
1400 return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
76af15bb 1401 }
76af15bb 1402 }
2ede466e 1403 return false;
76af15bb
MN
1404 }
1405
117bd748
PS
1406 /**
1407 * For testing only. Wipes information cached in user session.
3564771d 1408 *
9ccadad3 1409 * @global stdClass $SESSION
3564771d 1410 */
82bd6a5e 1411 static function wipe_session_cache() {
1412 global $SESSION;
1413 unset($SESSION->gradescorecache);
1414 unset($SESSION->gradescorecacheuserid);
76af15bb 1415 unset($SESSION->userfieldcache);
33f30dec 1416 unset($SESSION->userfieldcacheuserid);
82bd6a5e 1417 }
1418
1419 /**
ce4dfd27 1420 * Initialises the global cache
1421 * @global stdClass $CONDITIONLIB_PRIVATE
1422 */
94dc3c7d 1423 public static function init_global_cache() {
ce4dfd27 1424 global $CONDITIONLIB_PRIVATE;
1425 $CONDITIONLIB_PRIVATE = new stdClass;
1426 $CONDITIONLIB_PRIVATE->usedincondition = array();
1427 $CONDITIONLIB_PRIVATE->groupingscache = array();
1428 }
1429
1430 /**
1431 * Utility function that resets grade/completion conditions in table based
1432 * in data from editing form.
82bd6a5e 1433 *
ce4dfd27 1434 * @param condition_info_base $ci Condition info
1435 * @param object $fromform Data from form
1436 * @param bool $wipefirst If true, wipes existing conditions
82bd6a5e 1437 */
ce4dfd27 1438 protected static function update_from_form(condition_info_base $ci, $fromform, $wipefirst) {
373fb9dd 1439 if ($wipefirst) {
82bd6a5e 1440 $ci->wipe_conditions();
1441 }
373fb9dd 1442 foreach ($fromform->conditiongradegroup as $record) {
82bd6a5e 1443 if($record['conditiongradeitemid']) {
1444 $ci->add_grade_condition($record['conditiongradeitemid'],
ce4dfd27 1445 unformat_float($record['conditiongrademin']), unformat_float($record['conditiongrademax']));
82bd6a5e 1446 }
1447 }
76af15bb
MN
1448 foreach ($fromform->conditionfieldgroup as $record) {
1449 if($record['conditionfield']) {
1450 $ci->add_user_field_condition($record['conditionfield'],
1451 $record['conditionfieldoperator'],
1452 $record['conditionfieldvalue']);
1453 }
1454 }
373fb9dd 1455 if(isset ($fromform->conditioncompletiongroup)) {
82bd6a5e 1456 foreach($fromform->conditioncompletiongroup as $record) {
1457 if($record['conditionsourcecmid']) {
1458 $ci->add_completion_condition($record['conditionsourcecmid'],
1459 $record['conditionrequiredcompletion']);
1460 }
1461 }
1462 }
1463 }
acfee0d4 1464
1465 /**
1466 * Obtains context for any necessary checks.
1467 *
1468 * @return context Suitable context for the item
1469 */
1470 protected abstract function get_context();
82bd6a5e 1471}
ce4dfd27 1472
1473condition_info::init_global_cache();