2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Used for tracking conditions that apply before activities are displayed
19 * to students ('conditional availability').
21 * @package core_condition
23 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') ||
die();
30 * CONDITION_STUDENTVIEW_HIDE - The activity is not displayed to students at all when conditions aren't met.
32 define('CONDITION_STUDENTVIEW_HIDE', 0);
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.
37 define('CONDITION_STUDENTVIEW_SHOW', 1);
40 * CONDITION_MISSING_NOTHING - The $item variable is expected to contain all completion-related data
42 define('CONDITION_MISSING_NOTHING', 0);
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
47 define('CONDITION_MISSING_EXTRATABLE', 1);
49 * CONDITION_MISSING_EVERYTHING - The $item variable is expected to contain nothing except the ID
51 define('CONDITION_MISSING_EVERYTHING', 2);
54 * OP_CONTAINS - comparison operator that determines whether a specified user field contains
57 define('OP_CONTAINS', 'contains');
59 * OP_DOES_NOT_CONTAIN - comparison operator that determines whether a specified user field does not
60 * contain a provided variable
62 define('OP_DOES_NOT_CONTAIN', 'doesnotcontain');
64 * OP_IS_EQUAL_TO - comparison operator that determines whether a specified user field is equal to
67 define('OP_IS_EQUAL_TO', 'isequalto');
69 * OP_STARTS_WITH - comparison operator that determines whether a specified user field starts with
72 define('OP_STARTS_WITH', 'startswith');
74 * OP_ENDS_WITH - comparison operator that determines whether a specified user field ends with
77 define('OP_ENDS_WITH', 'endswith');
79 * OP_IS_EMPTY - comparison operator that determines whether a specified user field is empty
81 define('OP_IS_EMPTY', 'isempty');
83 * OP_IS_NOT_EMPTY - comparison operator that determines whether a specified user field is not empty
85 define('OP_IS_NOT_EMPTY', 'isnotempty');
87 require_once($CFG->libdir
.'/completionlib.php');
90 * Core class to handle conditional activites
92 * @package core_condition
94 * @copyright 2012 The Open University
95 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
97 class condition_info
extends condition_info_base
{
99 * Constructs with course-module details.
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
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
115 public function __construct($cm, $expectingmissing = CONDITION_MISSING_NOTHING
,
117 parent
::__construct($cm, 'course_modules', 'coursemoduleid',
118 $expectingmissing, $loaddata);
122 * Adds the extra availability conditions (if any) into the given
123 * course-module (or section) object.
125 * This function may be called statically (for the editing form) or
128 * @param object $cm Moodle course-module data object
130 public static function fill_availability_conditions($cm) {
131 parent
::fill_availability_conditions_inner($cm, 'course_modules', 'coursemoduleid');
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
139 public function get_full_course_module() {
140 return $this->get_full_item();
144 * Utility function called by modedit.php; updates the
145 * course_modules_availability table based on the module form data.
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
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);
157 * Used in course/lib.php because we need to disable the completion JS if
158 * a completion value affects a conditional activity.
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
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();
175 foreach ($modinfo->cms
as $othercm) {
176 foreach ($othercm->conditionscompletion
as $cmid => $expectedcompletion) {
177 $CONDITIONLIB_PRIVATE->usedincondition
[$course->id
][$cmid] = true;
182 foreach ($modinfo->get_section_info_all() as $section) {
183 foreach ($section->conditionscompletion
as $cmid => $expectedcompletion) {
184 $CONDITIONLIB_PRIVATE->usedincondition
[$course->id
][$cmid] = true;
188 return array_key_exists($cm->id
, $CONDITIONLIB_PRIVATE->usedincondition
[$course->id
]);
191 protected function get_context() {
192 return context_module
::instance($this->item
->id
);
198 * Handles conditional access to sections.
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
205 class condition_info_section
extends condition_info_base
{
207 * Constructs with course-module details.
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
222 public function __construct($section, $expectingmissing = CONDITION_MISSING_NOTHING
,
224 parent
::__construct($section, 'course_sections', 'coursesectionid',
225 $expectingmissing, $loaddata);
229 * Adds the extra availability conditions (if any) into the given
230 * course-module (or section) object.
232 * This function may be called statically (for the editing form) or
235 * @param object $section Moodle section data object
237 public static function fill_availability_conditions($section) {
238 parent
::fill_availability_conditions_inner($section, 'course_sections', 'coursesectionid');
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
246 public function get_full_section() {
247 return $this->get_full_item();
251 * Gets list of required fields from main table.
252 * @return array Array of field names
254 protected function get_main_table_fields() {
255 return array_merge(parent
::get_main_table_fields(), array('groupingid'));
259 * Determines whether this particular section is currently available
260 * according to these criteria.
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.
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
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
)) {
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('
303 JOIN {groupings_groups} gg ON g.id = gg.groupingid
304 JOIN {groups_members} gm ON gg.groupid = gm.groupid
306 g.id = ? AND gm.userid = ?',
307 array($this->item
->groupingid
, $userid));
308 if (!$gotit && !has_capability('moodle/site:accessallgroups', $context, $userid)) {
310 $information .= get_string('groupingnoaccess', 'condition');
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;
318 $groupings = $DB->get_records_sql('
323 JOIN {groupings_groups} gg ON g.id = gg.groupingid
324 JOIN {groups_members} gm ON gg.groupid = gm.groupid
326 g.courseid = ? AND gm.userid = ?',
327 array($this->item
->course
, $userid));
329 foreach ($groupings as $grouping) {
330 $list[$grouping->gid
] = true;
332 $CONDITIONLIB_PRIVATE->groupingscache
[$this->item
->course
] = $list;
336 $usergroupings = $CONDITIONLIB_PRIVATE->groupingscache
[$this->item
->course
];
337 if ($usergroupings !== true && !array_key_exists($this->item
->groupingid
, $usergroupings)) {
339 $information .= get_string('groupingnoaccess', 'condition');
344 $information = trim($information);
349 * Utility function called by modedit.php; updates the
350 * course_modules_availability table based on the module form data.
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
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);
361 protected function get_context() {
362 return context_course
::instance($this->item
->course
);
368 * Base class to handle conditional items of some kind (currently either
369 * course_modules or sections; they must have a corresponding _availability
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
377 abstract class condition_info_base
{
382 protected $availtable;
384 protected $availfieldtable;
386 protected $idfieldname;
388 protected $usergroupings;
389 /** @var array An array of custom profile field ids => to their shortname */
390 protected $customprofilefields = null;
392 * Constructs with item details.
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
417 public function __construct($item, $tableprefix, $idfield, $expectingmissing, $loaddata) {
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');
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
436 $this->item
= (object)array('id' => $item->id
);
437 $this->gotdata
= false;
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;
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 ' .
455 '[This warning can be disabled, see phpdoc.]',
458 $item = $DB->get_record($tableprefix, array('id' => $item->id
),
459 implode(',', $basicfields), MUST_EXIST
);
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.]',
475 $this->fill_availability_conditions($this->item
);
480 * Gets list of required fields from main table.
482 * @return array Array of field names
484 protected function get_main_table_fields() {
485 return array('id', 'course', 'visible',
486 'availablefrom', 'availableuntil', 'showavailability');
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
497 protected static function fill_availability_conditions_inner($item, $tableprefix, $idfield) {
499 if (empty($item->id
)) {
500 throw new coding_exception('Invalid parameters; item ID not included');
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('
511 a.id AS aid, gi.*, a.sourcecmid, a.requiredcompletion, a.gradeitemid,
512 a.grademin as conditiongrademin, a.grademax as conditiongrademax
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
;
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;
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
;
546 $fieldname = '!missing';
549 $field = $condition->userfield
;
550 $fieldname = $condition->userfield
;
552 $details = new stdClass
;
553 $details->fieldname
= $fieldname;
554 $details->operator
= $condition->operator
;
555 $details->value
= $condition->value
;
556 $item->conditionsfield
[$field] = $details;
562 * Obtains the name of a grade item.
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
569 private static function get_grade_name($gradeitemobj) {
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();
577 return '!missing'; // Ooops, missing grade
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
586 protected function get_full_item() {
587 $this->require_data();
592 * The operators that provide the relationship
593 * between a field and a value.
595 * @return array Associative array from operator constant to display name
597 public static function get_condition_user_field_operators() {
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'),
610 * Returns list of user fields that can be compared.
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
617 * @param array $formatoptions Passed to format_string if provided
618 * @return array Associative array from user field constants to display name
620 public static function get_condition_user_fields($formatoptions = null) {
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')
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);
649 $userfields[$field->id
] = $field->name
;
658 * Adds to the database a condition based on completion of another module.
660 * @global moodle_database $DB
661 * @param int $cmid ID of other module
662 * @param int $requiredcompletion COMPLETION_xx constant
664 public function add_completion_condition($cmid, $requiredcompletion) {
667 $DB->insert_record($this->availtable
, (object)array(
668 $this->idfieldname
=> $this->item
->id
,
669 'sourcecmid' => $cmid, 'requiredcompletion' => $requiredcompletion),
672 // Store in memory too
673 $this->item
->conditionscompletion
[$cmid] = $requiredcompletion;
677 * Adds user fields condition
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
684 public function add_user_field_condition($field, $operator, $value) {
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;
698 $objavailfield->fieldname
= $field;
699 $objavailfield->userfield
= $field;
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;
710 * Adds to the database a condition based on the value of a grade item.
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)
720 public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
730 $DB->insert_record($this->availtable
, (object)array(
731 $this->idfieldname
=> $this->item
->id
,
732 'gradeitemid' => $gradeitemid, 'grademin' => $min, 'grademax' => $max),
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)));
745 * Erases from the database all conditions for this activity.
747 * @global moodle_database $DB
749 public function wipe_conditions() {
753 $DB->delete_records($this->availtable
, array($this->idfieldname
=> $this->item
->id
));
754 $DB->delete_records($this->availfieldtable
, array($this->idfieldname
=> $this->item
->id
));
757 $this->item
->conditionsgrade
= array();
758 $this->item
->conditionscompletion
= array();
759 $this->item
->conditionsfield
= array();
763 * Obtains a string describing all availability restrictions (even if
764 * they do not apply any more).
766 * @global stdClass $COURSE
767 * @global moodle_database $DB
768 * @param object $modinfo Usually leave as null for default. Specify when
769 * calling recursively from inside get_fast_modinfo. The value supplied
770 * here must include list of all CMs with 'id' and 'name'
771 * @return string Information string (for admin) about all restrictions on
774 public function get_full_information($modinfo=null) {
776 $this->require_data();
781 // Completion conditions
782 if (count($this->item
->conditionscompletion
) > 0) {
783 if ($this->item
->course
== $COURSE->id
) {
786 $course = $DB->get_record('course', array('id' => $this->item
->course
),
787 'id, enablecompletion, modinfo, sectioncache', MUST_EXIST
);
789 foreach ($this->item
->conditionscompletion
as $cmid => $expectedcompletion) {
791 $modinfo = get_fast_modinfo($course);
793 if (empty($modinfo->cms
[$cmid])) {
796 $information .= html_writer
::start_tag('li');
797 $information .= get_string(
798 'requires_completion_' . $expectedcompletion,
799 'condition', $modinfo->cms
[$cmid]->name
) . ' ';
800 $information .= html_writer
::end_tag('li');
805 if (count($this->item
->conditionsgrade
) > 0) {
806 foreach ($this->item
->conditionsgrade
as $gradeitemid => $minmax) {
807 // String depends on type of requirement. We are coy about
808 // the actual numbers, in case grades aren't released to
810 if (is_null($minmax->min
) && is_null($minmax->max
)) {
812 } else if (is_null($minmax->max
)) {
814 } else if (is_null($minmax->min
)) {
819 $information .= html_writer
::start_tag('li');
820 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name
).' ';
821 $information .= html_writer
::end_tag('li');
825 // User field conditions
826 if (count($this->item
->conditionsfield
) > 0) {
827 $context = $this->get_context();
828 // Need the array of operators
829 foreach ($this->item
->conditionsfield
as $field => $details) {
831 // Display the fieldname into current lang.
832 $translatedfieldname = get_user_field_name($details->fieldname
);
833 $a->field
= format_string($translatedfieldname, true, array('context' => $context));
834 $a->value
= s($details->value
);
835 $information .= html_writer
::start_tag('li');
836 $information .= get_string('requires_user_field_'.$details->operator
, 'condition', $a) . ' ';
837 $information .= html_writer
::end_tag('li');
841 // The date logic is complicated. The intention of this logic is:
842 // 1) display date without time where possible (whenever the date is
844 // 2) when the 'until' date is e.g. 00:00 on the 14th, we display it as
845 // 'until the 13th' (experience at the OU showed that students are
846 // likely to interpret 'until <date>' as 'until the end of <date>').
847 // 3) This behaviour becomes confusing for 'same-day' dates where there
848 // are some exceptions.
849 // Users in different time zones will typically not get the 'abbreviated'
850 // behaviour but it should work OK for them aside from that.
852 // The following cases are possible:
853 // a) From 13:05 on 14 Oct until 12:10 on 17 Oct (exact, exact)
854 // b) From 14 Oct until 12:11 on 17 Oct (midnight, exact)
855 // c) From 13:05 on 14 Oct until 17 Oct (exact, midnight 18 Oct)
856 // d) From 14 Oct until 17 Oct (midnight 14 Oct, midnight 18 Oct)
857 // e) On 14 Oct (midnight 14 Oct, midnight 15 Oct)
858 // f) From 13:05 on 14 Oct until 0:00 on 15 Oct (exact, midnight, same day)
859 // g) From 0:00 on 14 Oct until 12:05 on 14 Oct (midnight, exact, same day)
860 // h) From 13:05 on 14 Oct (exact)
861 // i) From 14 Oct (midnight)
862 // j) Until 13:05 on 14 Oct (exact)
863 // k) Until 14 Oct (midnight 15 Oct)
865 // Check if start and end dates are 'midnights', if so we show in short form
866 $shortfrom = self
::is_midnight($this->item
->availablefrom
);
867 $shortuntil = self
::is_midnight($this->item
->availableuntil
);
869 // For some checks and for display, we need the previous day for the 'until'
870 // value, if we are going to display it in short form
871 if ($this->item
->availableuntil
) {
872 $daybeforeuntil = strtotime('-1 day', usergetmidnight($this->item
->availableuntil
));
875 // Special case for if one but not both are exact and they are within a day
876 if ($this->item
->availablefrom
&& $this->item
->availableuntil
&&
877 $shortfrom != $shortuntil && $daybeforeuntil < $this->item
->availablefrom
) {
878 // Don't use abbreviated version (see examples f, g above)
883 // When showing short end date, the display time is the 'day before' one
884 $displayuntil = $shortuntil ?
$daybeforeuntil : $this->item
->availableuntil
;
886 if ($this->item
->availablefrom
&& $this->item
->availableuntil
) {
887 if ($shortfrom && $shortuntil && $daybeforeuntil == $this->item
->availablefrom
) {
888 $information .= html_writer
::start_tag('li');
889 $information .= get_string('requires_date_both_single_day', 'condition',
890 self
::show_time($this->item
->availablefrom
, true));
891 $information .= html_writer
::end_tag('li');
893 $information .= html_writer
::start_tag('li');
894 $information .= get_string('requires_date_both', 'condition', (object)array(
895 'from' => self
::show_time($this->item
->availablefrom
, $shortfrom),
896 'until' => self
::show_time($displayuntil, $shortuntil)));
897 $information .= html_writer
::end_tag('li');
899 } else if ($this->item
->availablefrom
) {
900 $information .= html_writer
::start_tag('li');
901 $information .= get_string('requires_date', 'condition',
902 self
::show_time($this->item
->availablefrom
, $shortfrom));
903 $information .= html_writer
::end_tag('li');
904 } else if ($this->item
->availableuntil
) {
905 $information .= html_writer
::start_tag('li');
906 $information .= get_string('requires_date_before', 'condition',
907 self
::show_time($displayuntil, $shortuntil));
908 $information .= html_writer
::end_tag('li');
911 // The information is in <li> tags, but to avoid taking up more space
912 // if there is only a single item, we strip out the list tags so that it
913 // is plain text in that case.
914 if (!empty($information)) {
915 $li = strpos($information, '<li>', 4);
917 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
919 $information = html_writer
::tag('ul', $information);
921 $information = trim($information);
927 * Checks whether a given time refers exactly to midnight (in current user
930 * @param int $time Time
931 * @return bool True if time refers to midnight, false if it's some other
932 * time or if it is set to zero
934 private static function is_midnight($time) {
935 return $time && usergetmidnight($time) == $time;
939 * Determines whether this particular item is currently available
940 * according to these criteria.
942 * - This does not include the 'visible' setting (i.e. this might return
943 * true even if visible is false); visible is handled independently.
944 * - This does not take account of the viewhiddenactivities capability.
945 * That should apply later.
947 * @global stdClass $COURSE
948 * @global moodle_database $DB
949 * @uses COMPLETION_COMPLETE
950 * @uses COMPLETION_COMPLETE_FAIL
951 * @uses COMPLETION_COMPLETE_PASS
952 * @param string $information If the item has availability restrictions,
953 * a string that describes the conditions will be stored in this variable;
954 * if this variable is set blank, that means don't display anything
955 * @param bool $grabthelot Performance hint: if true, caches information
956 * required for all course-modules, to make the front page and similar
957 * pages work more quickly (works only for current user)
958 * @param int $userid If set, specifies a different user ID to check availability for
959 * @param object $modinfo Usually leave as null for default. Specify when
960 * calling recursively from inside get_fast_modinfo. The value supplied
961 * here must include list of all CMs with 'id' and 'name'
962 * @return bool True if this item is available to the user, false otherwise
964 public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
966 $this->require_data();
971 // Check each completion condition
972 if (count($this->item
->conditionscompletion
) > 0) {
973 if ($this->item
->course
== $COURSE->id
) {
976 $course = $DB->get_record('course', array('id' => $this->item
->course
),
977 'id, enablecompletion, modinfo, sectioncache', MUST_EXIST
);
980 $completion = new completion_info($course);
981 foreach ($this->item
->conditionscompletion
as $cmid => $expectedcompletion) {
982 // If this depends on a deleted module, handle that situation
985 $modinfo = get_fast_modinfo($course);
987 if (empty($modinfo->cms
[$cmid])) {
989 if (isset($PAGE) && strpos($PAGE->pagetype
, 'course-view-')===0) {
990 debugging("Warning: activity {$this->item->id} '{$this->item->name}' has condition " .
991 "on deleted activity $cmid (to get rid of this message, edit the named activity)");
996 // The completion system caches its own data
997 $completiondata = $completion->get_data((object)array('id' => $cmid),
998 $grabthelot, $userid, $modinfo);
1001 if ($expectedcompletion==COMPLETION_COMPLETE
) {
1002 // 'Complete' also allows the pass, fail states
1003 switch ($completiondata->completionstate
) {
1004 case COMPLETION_COMPLETE
:
1005 case COMPLETION_COMPLETE_FAIL
:
1006 case COMPLETION_COMPLETE_PASS
:
1012 // Other values require exact match
1013 if ($completiondata->completionstate
!=$expectedcompletion) {
1019 $information .= html_writer
::start_tag('li');
1020 $information .= get_string(
1021 'requires_completion_' . $expectedcompletion,
1022 'condition', $modinfo->cms
[$cmid]->name
) . ' ';
1023 $information .= html_writer
::end_tag('li');
1028 // Check each grade condition
1029 if (count($this->item
->conditionsgrade
)>0) {
1030 foreach ($this->item
->conditionsgrade
as $gradeitemid => $minmax) {
1031 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
1032 if ($score===false ||
1033 (!is_null($minmax->min
) && $score<$minmax->min
) ||
1034 (!is_null($minmax->max
) && $score>=$minmax->max
)) {
1037 // String depends on type of requirement. We are coy about
1038 // the actual numbers, in case grades aren't released to
1040 if (is_null($minmax->min
) && is_null($minmax->max
)) {
1042 } else if (is_null($minmax->max
)) {
1044 } else if (is_null($minmax->min
)) {
1049 $information .= html_writer
::start_tag('li');
1050 $information .= get_string('requires_grade_' . $string, 'condition', $minmax->name
) . ' ';
1051 $information .= html_writer
::end_tag('li');
1056 // Check if user field condition
1057 if (count($this->item
->conditionsfield
) > 0) {
1058 $context = $this->get_context();
1059 foreach ($this->item
->conditionsfield
as $field => $details) {
1060 $uservalue = $this->get_cached_user_profile_field($userid, $field);
1061 if (!$this->is_field_condition_met($details->operator
, $uservalue, $details->value
)) {
1062 // Set available to false
1064 $a = new stdClass();
1065 $a->field
= format_string($details->fieldname
, true, array('context' => $context));
1066 $a->value
= s($details->value
);
1067 $information .= html_writer
::start_tag('li');
1068 $information .= get_string('requires_user_field_'.$details->operator
, 'condition', $a) . ' ';
1069 $information .= html_writer
::end_tag('li');
1075 if ($this->item
->availablefrom
) {
1076 if (time() < $this->item
->availablefrom
) {
1079 $information .= html_writer
::start_tag('li');
1080 $information .= get_string('requires_date', 'condition',
1081 self
::show_time($this->item
->availablefrom
,
1082 self
::is_midnight($this->item
->availablefrom
)));
1083 $information .= html_writer
::end_tag('li');
1087 if ($this->item
->availableuntil
) {
1088 if (time() >= $this->item
->availableuntil
) {
1090 // But we don't display any information about this case. This is
1091 // because the only reason to set a 'disappear' date is usually
1092 // to get rid of outdated information/clutter in which case there
1093 // is no point in showing it...
1095 // Note it would be nice if we could make it so that the 'until'
1096 // date appears below the item while the item is still accessible,
1097 // unfortunately this is not possible in the current system. Maybe
1098 // later, or if somebody else wants to add it.
1102 // If the item is marked as 'not visible' then we don't change the available
1103 // flag (visible/available are treated distinctly), but we remove any
1104 // availability info. If the item is hidden with the eye icon, it doesn't
1105 // make sense to show 'Available from <date>' or similar, because even
1106 // when that date arrives it will still not be available unless somebody
1107 // toggles the eye icon.
1108 if (!$this->item
->visible
) {
1112 // The information is in <li> tags, but to avoid taking up more space
1113 // if there is only a single item, we strip out the list tags so that it
1114 // is plain text in that case.
1115 if (!empty($information)) {
1116 $li = strpos($information, '<li>', 4);
1117 if ($li === false) {
1118 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
1120 $information = html_writer
::tag('ul', $information);
1122 $information = trim($information);
1128 * Shows a time either as a date or a full date and time, according to
1131 * @param int $time Time
1132 * @param bool $dateonly If true, uses date only
1133 * @return string Date
1135 private function show_time($time, $dateonly) {
1136 return userdate($time,
1137 get_string($dateonly ?
'strftimedate' : 'strftimedatetime', 'langconfig'));
1141 * Checks whether availability information should be shown to normal users.
1143 * @return bool True if information about availability should be shown to
1145 * @throws coding_exception If data wasn't loaded
1147 public function show_availability() {
1148 $this->require_data();
1149 return $this->item
->showavailability
;
1153 * Internal function cheks that data was loaded.
1155 * @throws coding_exception If data wasn't loaded
1157 private function require_data() {
1158 if (!$this->gotdata
) {
1159 throw new coding_exception('Error: cannot call when info was ' .
1160 'constructed without data');
1165 * Obtains a grade score. Note that this score should not be displayed to
1166 * the user, because gradebook rules might prohibit that. It may be a
1167 * non-final score subject to adjustment later.
1169 * @global stdClass $USER
1170 * @global moodle_database $DB
1171 * @global stdClass $SESSION
1172 * @param int $gradeitemid Grade item ID we're interested in
1173 * @param bool $grabthelot If true, grabs all scores for current user on
1174 * this course, so that later ones come from cache
1175 * @param int $userid Set if requesting grade for a different user (does
1177 * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
1178 * or 37.21), or false if user does not have a grade yet
1180 private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
1181 global $USER, $DB, $SESSION;
1182 if ($userid==0 ||
$userid==$USER->id
) {
1183 // For current user, go via cache in session
1184 if (empty($SESSION->gradescorecache
) ||
$SESSION->gradescorecacheuserid
!=$USER->id
) {
1185 $SESSION->gradescorecache
= array();
1186 $SESSION->gradescorecacheuserid
= $USER->id
;
1188 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache
)) {
1190 // Get all grades for the current course
1191 $rs = $DB->get_recordset_sql('
1193 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
1196 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
1198 gi.courseid = ?', array($USER->id
, $this->item
->course
));
1199 foreach ($rs as $record) {
1200 $SESSION->gradescorecache
[$record->id
] =
1201 is_null($record->finalgrade
)
1204 // Otherwise convert grade to percentage
1205 : (($record->finalgrade
- $record->rawgrademin
) * 100) /
1206 ($record->rawgrademax
- $record->rawgrademin
);
1210 // And if it's still not set, well it doesn't exist (eg
1211 // maybe the user set it as a condition, then deleted the
1212 // grade item) so we call it false
1213 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache
)) {
1214 $SESSION->gradescorecache
[$gradeitemid] = false;
1217 // Just get current grade
1218 $record = $DB->get_record('grade_grades', array(
1219 'userid'=>$USER->id
, 'itemid'=>$gradeitemid));
1220 if ($record && !is_null($record->finalgrade
)) {
1221 $score = (($record->finalgrade
- $record->rawgrademin
) * 100) /
1222 ($record->rawgrademax
- $record->rawgrademin
);
1224 // Treat the case where row exists but is null, same as
1225 // case where row doesn't exist
1228 $SESSION->gradescorecache
[$gradeitemid]=$score;
1231 return $SESSION->gradescorecache
[$gradeitemid];
1233 // Not the current user, so request the score individually
1234 $record = $DB->get_record('grade_grades', array(
1235 'userid'=>$userid, 'itemid'=>$gradeitemid));
1236 if ($record && !is_null($record->finalgrade
)) {
1237 $score = (($record->finalgrade
- $record->rawgrademin
) * 100) /
1238 ($record->rawgrademax
- $record->rawgrademin
);
1240 // Treat the case where row exists but is null, same as
1241 // case where row doesn't exist
1249 * Returns true if a field meets the required conditions, false otherwise.
1251 * @param string $operator the requirement/condition
1252 * @param string $uservalue the user's value
1253 * @param string $value the value required
1256 private function is_field_condition_met($operator, $uservalue, $value) {
1257 if ($uservalue === false) {
1258 // If the user value is false this is an instant fail.
1259 // All user values come from the database as either data or the default.
1260 // They will always be a string.
1263 $fieldconditionmet = true;
1264 // Just to be doubly sure it is a string.
1265 $uservalue = (string)$uservalue;
1267 case OP_CONTAINS
: // contains
1268 $pos = strpos($uservalue, $value);
1269 if ($pos === false) {
1270 $fieldconditionmet = false;
1273 case OP_DOES_NOT_CONTAIN
: // does not contain
1274 if (!empty($value)) {
1275 $pos = strpos($uservalue, $value);
1276 if ($pos !== false) {
1277 $fieldconditionmet = false;
1281 case OP_IS_EQUAL_TO
: // equal to
1282 if ($value !== $uservalue) {
1283 $fieldconditionmet = false;
1286 case OP_STARTS_WITH
: // starts with
1287 $length = strlen($value);
1288 if ((substr($uservalue, 0, $length) !== $value)) {
1289 $fieldconditionmet = false;
1292 case OP_ENDS_WITH
: // ends with
1293 $length = strlen($value);
1294 $start = $length * -1; // negative
1295 if (substr($uservalue, $start) !== $value) {
1296 $fieldconditionmet = false;
1299 case OP_IS_EMPTY
: // is empty
1300 if (!empty($uservalue)) {
1301 $fieldconditionmet = false;
1304 case OP_IS_NOT_EMPTY
: // is not empty
1305 if (empty($uservalue)) {
1306 $fieldconditionmet = false;
1310 return $fieldconditionmet;
1314 * Return the value for a user's profile field
1316 * @param int $userid set if requesting grade for a different user (does not use cache)
1317 * @param int $fieldid the user profile field id
1318 * @return string the user value, or false if user does not have a user field value yet
1320 protected function get_cached_user_profile_field($userid, $fieldid) {
1321 global $USER, $DB, $CFG;
1323 if ($userid === 0) {
1324 // Map out userid = 0 to the current user
1325 $userid = $USER->id
;
1327 $iscurrentuser = $USER->id
== $userid;
1329 if (isguestuser($userid) ||
($iscurrentuser && !isloggedin())) {
1330 // Must be logged in and can't be the guest. (e.g. front page)
1334 // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
1335 $iscustomprofilefield = is_numeric($fieldid);
1336 if ($iscustomprofilefield) {
1337 // As its a custom profile field we need to map the id back to the actual field.
1338 // We'll also preload all of the other custom profile fields just in case and ensure we have the
1339 // default value available as well.
1340 if ($this->customprofilefields
=== null) {
1341 $this->customprofilefields
= $DB->get_records('user_info_field', null, 'sortorder ASC, id ASC', 'id, shortname, defaultdata');
1343 if (!array_key_exists($fieldid, $this->customprofilefields
)) {
1344 // No such field exists.
1345 // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
1346 // or when restoring a backup of a course with user profile field conditions.
1349 $field = $this->customprofilefields
[$fieldid]->shortname
;
1354 // If its the current user than most likely we will be able to get this information from $USER.
1355 // If its a regular profile field then it should already be available, if not then we have a mega problem.
1356 // If its a custom profile field then it should be available but may not be. If it is then we use the value
1357 // available, otherwise we load all custom profile fields into a temp object and refer to that.
1358 // Noting its not going be great for performance if we have to use the temp object as it involves loading the
1359 // custom profile field API and classes.
1360 if ($iscurrentuser) {
1361 if (!$iscustomprofilefield) {
1362 if (property_exists($USER, $field)) {
1363 return $USER->{$field};
1365 // Unknown user field. This should not happen.
1366 throw new coding_exception('Requested user profile field does not exist');
1369 // Checking if the custom profile fields are already available.
1370 if (!isset($USER->profile
)) {
1371 // Drat! they're not. We need to use a temp object and load them.
1372 // We don't use $USER as the profile fields are loaded into the object.
1373 $user = new stdClass
;
1374 $user->id
= $USER->id
;
1375 // This should ALWAYS be set, but just in case we check.
1376 require_once($CFG->dirroot
.'/user/profile/lib.php');
1377 profile_load_custom_fields($user);
1378 if (array_key_exists($field, $user->profile
)) {
1379 return $user->profile
[$field];
1381 } else if (array_key_exists($field, $USER->profile
)) {
1382 // Hurrah they're available, this is easy.
1383 return $USER->profile
[$field];
1385 // The profile field doesn't exist.
1388 // Loading for another user.
1389 if ($iscustomprofilefield) {
1390 // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
1391 // for us (this will likely be hit again).
1392 // We are able to do this because we've already pre-loaded the custom fields.
1393 $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 'fieldid' => $fieldid), IGNORE_MISSING
);
1394 // If we have data return that, otherwise return the default.
1395 if ($data !== false) {
1398 return $this->customprofilefields
[$field]->defaultdata
;
1401 // Its a standard field, retrieve it from the user.
1402 return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST
);
1409 * For testing only. Wipes information cached in user session.
1411 * @global stdClass $SESSION
1413 static function wipe_session_cache() {
1415 unset($SESSION->gradescorecache
);
1416 unset($SESSION->gradescorecacheuserid
);
1417 unset($SESSION->userfieldcache
);
1418 unset($SESSION->userfieldcacheuserid
);
1422 * Initialises the global cache
1423 * @global stdClass $CONDITIONLIB_PRIVATE
1425 public static function init_global_cache() {
1426 global $CONDITIONLIB_PRIVATE;
1427 $CONDITIONLIB_PRIVATE = new stdClass
;
1428 $CONDITIONLIB_PRIVATE->usedincondition
= array();
1429 $CONDITIONLIB_PRIVATE->groupingscache
= array();
1433 * Utility function that resets grade/completion conditions in table based
1434 * in data from editing form.
1436 * @param condition_info_base $ci Condition info
1437 * @param object $fromform Data from form
1438 * @param bool $wipefirst If true, wipes existing conditions
1440 protected static function update_from_form(condition_info_base
$ci, $fromform, $wipefirst) {
1442 $ci->wipe_conditions();
1444 foreach ($fromform->conditiongradegroup
as $record) {
1445 if($record['conditiongradeitemid']) {
1446 $ci->add_grade_condition($record['conditiongradeitemid'],
1447 unformat_float($record['conditiongrademin']), unformat_float($record['conditiongrademax']));
1450 foreach ($fromform->conditionfieldgroup
as $record) {
1451 if($record['conditionfield']) {
1452 $ci->add_user_field_condition($record['conditionfield'],
1453 $record['conditionfieldoperator'],
1454 $record['conditionfieldvalue']);
1457 if(isset ($fromform->conditioncompletiongroup
)) {
1458 foreach($fromform->conditioncompletiongroup
as $record) {
1459 if($record['conditionsourcecmid']) {
1460 $ci->add_completion_condition($record['conditionsourcecmid'],
1461 $record['conditionrequiredcompletion']);
1468 * Obtains context for any necessary checks.
1470 * @return context Suitable context for the item
1472 protected abstract function get_context();
1475 condition_info
::init_global_cache();