MDL-24471, FILEMANAGER change <button> to <input type="button" />, moodle form may...
[moodle.git] / lib / conditionlib.php
blobb4fa3195440bc534907b38da9ed1fd293802a4f2
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Used for tracking conditions that apply before activities are displayed
19 * to students ('conditional availability').
21 * @package core
22 * @subpackage completion
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();
29 /** The activity is not displayed to students at all when conditions aren't met. */
30 define('CONDITION_STUDENTVIEW_HIDE',0);
31 /** The activity is displayed to students as a greyed-out name, with informational
32 text that explains the conditions under which it will be available. */
33 define('CONDITION_STUDENTVIEW_SHOW',1);
35 /** The $cm variable is expected to contain all completion-related data */
36 define('CONDITION_MISSING_NOTHING',0);
37 /** The $cm variable is expected to contain the fields from course_modules but
38 not the course_modules_availability data */
39 define('CONDITION_MISSING_EXTRATABLE',1);
40 /** The $cm variable is expected to contain nothing except the ID */
41 define('CONDITION_MISSING_EVERYTHING',2);
43 /**
44 * @global stdClass $CONDITIONLIB_PRIVATE
45 * @name $CONDITIONLIB_PRIVATE
47 global $CONDITIONLIB_PRIVATE;
48 $CONDITIONLIB_PRIVATE = new stdClass;
49 // Caches whether completion values are used in availability conditions.
50 // Array of course => array of cmid => true.
51 $CONDITIONLIB_PRIVATE->usedincondition = array();
53 /**
54 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55 * @package moodlecore
57 class condition_info {
58 /**
59 * @var object, bool
61 private $cm, $gotdata;
63 /**
64 * Constructs with course-module details.
66 * @global object
67 * @uses CONDITION_MISSING_NOTHING
68 * @uses CONDITION_MISSING_EVERYTHING
69 * @uses DEBUG_DEVELOPER
70 * @uses CONDITION_MISSING_EXTRATABLE
71 * @param object $cm Moodle course-module object. May have extra fields
72 * ->conditionsgrade, ->conditionscompletion which should come from
73 * get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
74 * and ->showavailability, ->course; but the only required thing is ->id.
75 * @param int $expectingmissing Used to control whether or not a developer
76 * debugging message (performance warning) will be displayed if some of
77 * the above data is missing and needs to be retrieved; a
78 * CONDITION_MISSING_xx constant
79 * @param bool $loaddata If you need a 'write-only' object, set this value
80 * to false to prevent database access from constructor
81 * @return condition_info Object which can retrieve information about the
82 * activity
84 public function __construct($cm, $expectingmissing=CONDITION_MISSING_NOTHING,
85 $loaddata=true) {
86 global $DB;
88 // Check ID as otherwise we can't do the other queries
89 if (empty($cm->id)) {
90 throw new coding_exception("Invalid parameters; course-module ID not included");
93 // If not loading data, don't do anything else
94 if (!$loaddata) {
95 $this->cm = (object)array('id'=>$cm->id);
96 $this->gotdata = false;
97 return;
100 // Missing basic data from course_modules
101 if (!isset($cm->availablefrom) || !isset($cm->availableuntil) ||
102 !isset($cm->showavailability) || !isset($cm->course)) {
103 if ($expectingmissing<CONDITION_MISSING_EVERYTHING) {
104 debugging('Performance warning: condition_info constructor is
105 faster if you pass in $cm with at least basic fields
106 (availablefrom,availableuntil,showavailability,course).
107 [This warning can be disabled, see phpdoc.]',
108 DEBUG_DEVELOPER);
110 $cm = $DB->get_record('course_modules',array('id'=>$cm->id),
111 'id,course,availablefrom,availableuntil,showavailability');
114 $this->cm = clone($cm);
115 $this->gotdata = true;
117 // Missing extra data
118 if (!isset($cm->conditionsgrade) || !isset($cm->conditionscompletion)) {
119 if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) {
120 debugging('Performance warning: condition_info constructor is
121 faster if you pass in a $cm from get_fast_modinfo.
122 [This warning can be disabled, see phpdoc.]',
123 DEBUG_DEVELOPER);
126 self::fill_availability_conditions($this->cm);
131 * Adds the extra availability conditions (if any) into the given
132 * course-module object.
134 * @global object
135 * @global object
136 * @param object $cm Moodle course-module data object
138 public static function fill_availability_conditions(&$cm) {
139 if (empty($cm->id)) {
140 throw new coding_exception("Invalid parameters; course-module ID not included");
143 // Does nothing if the variables are already present
144 if (!isset($cm->conditionsgrade) ||
145 !isset($cm->conditionscompletion)) {
146 $cm->conditionsgrade=array();
147 $cm->conditionscompletion=array();
149 global $DB, $CFG;
150 $conditions = $DB->get_records_sql($sql="
151 SELECT
152 cma.id as cmaid, gi.*,cma.sourcecmid,cma.requiredcompletion,cma.gradeitemid,
153 cma.grademin as conditiongrademin, cma.grademax as conditiongrademax
154 FROM
155 {course_modules_availability} cma
156 LEFT JOIN {grade_items} gi ON gi.id=cma.gradeitemid
157 WHERE
158 coursemoduleid=?",array($cm->id));
159 foreach ($conditions as $condition) {
160 if (!is_null($condition->sourcecmid)) {
161 $cm->conditionscompletion[$condition->sourcecmid] =
162 $condition->requiredcompletion;
163 } else {
164 $minmax = new stdClass;
165 $minmax->min = $condition->conditiongrademin;
166 $minmax->max = $condition->conditiongrademax;
167 $minmax->name = self::get_grade_name($condition);
168 $cm->conditionsgrade[$condition->gradeitemid] = $minmax;
175 * Obtains the name of a grade item.
177 * @global object
178 * @param object $gradeitemobj Object from get_record on grade_items table,
179 * (can be empty if you want to just get !missing)
180 * @return string Name of item of !missing if it didn't exist
182 private static function get_grade_name($gradeitemobj) {
183 global $CFG;
184 if (isset($gradeitemobj->id)) {
185 require_once($CFG->libdir.'/gradelib.php');
186 $item = new grade_item;
187 grade_object::set_properties($item, $gradeitemobj);
188 return $item->get_name();
189 } else {
190 return '!missing'; // Ooops, missing grade
195 * @see require_data()
196 * @return object A course-module object with all the information required to
197 * determine availability.
199 public function get_full_course_module() {
200 $this->require_data();
201 return $this->cm;
205 * Adds to the database a condition based on completion of another module.
207 * @global object
208 * @param int $cmid ID of other module
209 * @param int $requiredcompletion COMPLETION_xx constant
211 public function add_completion_condition($cmid, $requiredcompletion) {
212 // Add to DB
213 global $DB;
214 $DB->insert_record('course_modules_availability',
215 (object)array('coursemoduleid'=>$this->cm->id,
216 'sourcecmid'=>$cmid, 'requiredcompletion'=>$requiredcompletion),
217 false);
219 // Store in memory too
220 $this->cm->conditionscompletion[$cmid] = $requiredcompletion;
224 * Adds to the database a condition based on the value of a grade item.
226 * @global object
227 * @param int $gradeitemid ID of grade item
228 * @param float $min Minimum grade (>=), up to 5 decimal points, or null if none
229 * @param float $max Maximum grade (<), up to 5 decimal points, or null if none
230 * @param bool $updateinmemory If true, updates data in memory; otherwise,
231 * memory version may be out of date (this has performance consequences,
232 * so don't do it unless it really needs updating)
234 public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
235 // Normalise nulls
236 if ($min==='') {
237 $min = null;
239 if ($max==='') {
240 $max = null;
242 // Add to DB
243 global $DB;
244 $DB->insert_record('course_modules_availability',
245 (object)array('coursemoduleid'=>$this->cm->id,
246 'gradeitemid'=>$gradeitemid, 'grademin'=>$min, 'grademax'=>$max),
247 false);
249 // Store in memory too
250 if ($updateinmemory) {
251 $this->cm->conditionsgrade[$gradeitemid]=(object)array(
252 'min'=>$min, 'max'=>$max);
253 $this->cm->conditionsgrade[$gradeitemid]->name =
254 self::get_grade_name($DB->get_record('grade_items',
255 array('id'=>$gradeitemid)));
260 * Erases from the database all conditions for this activity.
262 * @global object
264 public function wipe_conditions() {
265 // Wipe from DB
266 global $DB;
267 $DB->delete_records('course_modules_availability',
268 array('coursemoduleid'=>$this->cm->id));
270 // And from memory
271 $this->cm->conditionsgrade = array();
272 $this->cm->conditionscompletion = array();
276 * Obtains a string describing all availability restrictions (even if
277 * they do not apply any more).
279 * @global object
280 * @global object
281 * @param object $modinfo Usually leave as null for default. Specify when
282 * calling recursively from inside get_fast_modinfo. The value supplied
283 * here must include list of all CMs with 'id' and 'name'
284 * @return string Information string (for admin) about all restrictions on
285 * this item
287 public function get_full_information($modinfo=null) {
288 $this->require_data();
289 global $COURSE, $DB;
291 $information = '';
293 // Completion conditions
294 if(count($this->cm->conditionscompletion)>0) {
295 if ($this->cm->course==$COURSE->id) {
296 $course = $COURSE;
297 } else {
298 $course = $DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo');
300 foreach ($this->cm->conditionscompletion as $cmid=>$expectedcompletion) {
301 if (!$modinfo) {
302 $modinfo = get_fast_modinfo($course);
304 if (empty($modinfo->cms[$cmid])) {
305 continue;
307 $information .= get_string(
308 'requires_completion_'.$expectedcompletion,
309 'condition', $modinfo->cms[$cmid]->name).' ';
313 // Grade conditions
314 if (count($this->cm->conditionsgrade)>0) {
315 foreach ($this->cm->conditionsgrade as $gradeitemid=>$minmax) {
316 // String depends on type of requirement. We are coy about
317 // the actual numbers, in case grades aren't released to
318 // students.
319 if (is_null($minmax->min) && is_null($minmax->max)) {
320 $string = 'any';
321 } else if (is_null($minmax->max)) {
322 $string = 'min';
323 } else if (is_null($minmax->min)) {
324 $string = 'max';
325 } else {
326 $string = 'range';
328 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
332 // Dates
333 if ($this->cm->availablefrom && $this->cm->availableuntil) {
334 $information .= get_string('requires_date_both', 'condition',
335 (object)array(
336 'from' => self::show_time($this->cm->availablefrom, false),
337 'until' => self::show_time($this->cm->availableuntil, true)));
338 } else if ($this->cm->availablefrom) {
339 $information .= get_string('requires_date', 'condition',
340 self::show_time($this->cm->availablefrom, false));
341 } else if ($this->cm->availableuntil) {
342 $information .= get_string('requires_date_before', 'condition',
343 self::show_time($this->cm->availableuntil, true));
346 $information = trim($information);
347 return $information;
351 * Determines whether this particular course-module is currently available
352 * according to these criteria.
354 * - This does not include the 'visible' setting (i.e. this might return
355 * true even if visible is false); visible is handled independently.
356 * - This does not take account of the viewhiddenactivities capability.
357 * That should apply later.
359 * @global object
360 * @global object
361 * @uses COMPLETION_COMPLETE
362 * @uses COMPLETION_COMPLETE_FAIL
363 * @uses COMPLETION_COMPLETE_PASS
364 * @param string $information If the item has availability restrictions,
365 * a string that describes the conditions will be stored in this variable;
366 * if this variable is set blank, that means don't display anything
367 * @param bool $grabthelot Performance hint: if true, caches information
368 * required for all course-modules, to make the front page and similar
369 * pages work more quickly (works only for current user)
370 * @param int $userid If set, specifies a different user ID to check availability for
371 * @param object $modinfo Usually leave as null for default. Specify when
372 * calling recursively from inside get_fast_modinfo. The value supplied
373 * here must include list of all CMs with 'id' and 'name'
374 * @return bool True if this item is available to the user, false otherwise
376 public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
377 $this->require_data();
378 global $COURSE,$DB;
380 $available = true;
381 $information = '';
383 // Check each completion condition
384 if(count($this->cm->conditionscompletion)>0) {
385 if ($this->cm->course==$COURSE->id) {
386 $course = $COURSE;
387 } else {
388 $course = $DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo');
391 $completion = new completion_info($course);
392 foreach ($this->cm->conditionscompletion as $cmid=>$expectedcompletion) {
393 // If this depends on a deleted module, handle that situation
394 // gracefully.
395 if (!$modinfo) {
396 $modinfo = get_fast_modinfo($course);
398 if (empty($modinfo->cms[$cmid])) {
399 global $PAGE;
400 if (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0) {
401 debugging("Warning: activity {$this->cm->id} '{$this->cm->name}' has condition on deleted activity $cmid (to get rid of this message, edit the named activity)");
403 continue;
406 // The completion system caches its own data
407 $completiondata = $completion->get_data((object)array('id'=>$cmid),
408 $grabthelot, $userid, $modinfo);
410 $thisisok = true;
411 if ($expectedcompletion==COMPLETION_COMPLETE) {
412 // 'Complete' also allows the pass, fail states
413 switch ($completiondata->completionstate) {
414 case COMPLETION_COMPLETE:
415 case COMPLETION_COMPLETE_FAIL:
416 case COMPLETION_COMPLETE_PASS:
417 break;
418 default:
419 $thisisok = false;
421 } else {
422 // Other values require exact match
423 if ($completiondata->completionstate!=$expectedcompletion) {
424 $thisisok = false;
427 if (!$thisisok) {
428 $available = false;
429 $information .= get_string(
430 'requires_completion_'.$expectedcompletion,
431 'condition',$modinfo->cms[$cmid]->name).' ';
436 // Check each grade condition
437 if (count($this->cm->conditionsgrade)>0) {
438 foreach ($this->cm->conditionsgrade as $gradeitemid=>$minmax) {
439 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
440 if ($score===false ||
441 (!is_null($minmax->min) && $score<$minmax->min) ||
442 (!is_null($minmax->max) && $score>=$minmax->max)) {
443 // Grade fail
444 $available = false;
445 // String depends on type of requirement. We are coy about
446 // the actual numbers, in case grades aren't released to
447 // students.
448 if (is_null($minmax->min) && is_null($minmax->max)) {
449 $string = 'any';
450 } else if (is_null($minmax->max)) {
451 $string = 'min';
452 } else if (is_null($minmax->min)) {
453 $string = 'max';
454 } else {
455 $string = 'range';
457 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
462 // Test dates
463 if ($this->cm->availablefrom) {
464 if (time() < $this->cm->availablefrom) {
465 $available = false;
467 $information .= get_string('requires_date', 'condition',
468 self::show_time($this->cm->availablefrom, false));
472 if ($this->cm->availableuntil) {
473 if (time() >= $this->cm->availableuntil) {
474 $available = false;
475 // But we don't display any information about this case. This is
476 // because the only reason to set a 'disappear' date is usually
477 // to get rid of outdated information/clutter in which case there
478 // is no point in showing it...
480 // Note it would be nice if we could make it so that the 'until'
481 // date appears below the item while the item is still accessible,
482 // unfortunately this is not possible in the current system. Maybe
483 // later, or if somebody else wants to add it.
487 $information=trim($information);
488 return $available;
492 * Shows a time either as a date (if it falls exactly on the day) or
493 * a full date and time, according to user's timezone.
495 * @param int $time Time
496 * @param bool $until True if this date should be treated as the second of
497 * an inclusive pair - if so the time will be shown unless date is 23:59:59.
498 * Without this the date shows for 0:00:00.
499 * @return string Date
501 private function show_time($time, $until) {
502 // Break down the time into fields
503 $userdate = usergetdate($time);
505 // Handle the 'inclusive' second date
506 if($until) {
507 $dateonly = $userdate['hours']==23 && $userdate['minutes']==59 &&
508 $userdate['seconds']==59;
509 } else {
510 $dateonly = $userdate['hours']==0 && $userdate['minutes']==0 &&
511 $userdate['seconds']==0;
514 return userdate($time, get_string(
515 $dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
519 * @return bool True if information about availability should be shown to
520 * normal users
521 * @throws coding_exception If data wasn't loaded
523 public function show_availability() {
524 $this->require_data();
525 return $this->cm->showavailability;
529 * Internal function cheks that data was loaded.
531 * @return void throws coding_exception If data wasn't loaded
533 private function require_data() {
534 if (!$this->gotdata) {
535 throw new coding_exception('Error: cannot call when info was '.
536 'constructed without data');
541 * Obtains a grade score. Note that this score should not be displayed to
542 * the user, because gradebook rules might prohibit that. It may be a
543 * non-final score subject to adjustment later.
545 * @global object
546 * @global object
547 * @global object
548 * @param int $gradeitemid Grade item ID we're interested in
549 * @param bool $grabthelot If true, grabs all scores for current user on
550 * this course, so that later ones come from cache
551 * @param int $userid Set if requesting grade for a different user (does
552 * not use cache)
553 * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
554 * or 37.21), or false if user does not have a grade yet
556 private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
557 global $USER, $DB, $SESSION;
558 if ($userid==0 || $userid=$USER->id) {
559 // For current user, go via cache in session
560 if (empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) {
561 $SESSION->gradescorecache = array();
562 $SESSION->gradescorecacheuserid = $USER->id;
564 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
565 if ($grabthelot) {
566 // Get all grades for the current course
567 $rs = $DB->get_recordset_sql("
568 SELECT
569 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
570 FROM
571 {grade_items} gi
572 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
573 WHERE
574 gi.courseid=?", array($USER->id, $this->cm->course));
575 foreach ($rs as $record) {
576 $SESSION->gradescorecache[$record->id] =
577 is_null($record->finalgrade)
578 // No grade = false
579 ? false
580 // Otherwise convert grade to percentage
581 : (($record->finalgrade - $record->rawgrademin) * 100) /
582 ($record->rawgrademax - $record->rawgrademin);
585 $rs->close();
586 // And if it's still not set, well it doesn't exist (eg
587 // maybe the user set it as a condition, then deleted the
588 // grade item) so we call it false
589 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
590 $SESSION->gradescorecache[$gradeitemid] = false;
592 } else {
593 // Just get current grade
594 $record = $DB->get_record('grade_grades', array(
595 'userid'=>$USER->id, 'itemid'=>$gradeitemid));
596 if ($record && !is_null($record->finalgrade)) {
597 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
598 ($record->rawgrademax - $record->rawgrademin);
599 } else {
600 // Treat the case where row exists but is null, same as
601 // case where row doesn't exist
602 $score = false;
604 $SESSION->gradescorecache[$gradeitemid]=$score;
607 return $SESSION->gradescorecache[$gradeitemid];
608 } else {
609 // Not the current user, so request the score individually
610 $record = $DB->get_record('grade_grades', array(
611 'userid'=>$userid, 'itemid'=>$gradeitemid));
612 if ($record && !is_null($record->finalgrade)) {
613 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
614 ($record->rawgrademax - $record->rawgrademin);
615 } else {
616 // Treat the case where row exists but is null, same as
617 // case where row doesn't exist
618 $score = false;
620 return $score;
625 * For testing only. Wipes information cached in user session.
627 * @global object
629 static function wipe_session_cache() {
630 global $SESSION;
631 unset($SESSION->gradescorecache);
632 unset($SESSION->gradescorecacheuserid);
636 * Utility function called by modedit.php; updates the
637 * course_modules_availability table based on the module form data.
639 * @param object $cm Course-module with as much data as necessary, min id
640 * @param object $fromform
641 * @param bool $wipefirst Defaults to true
643 public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
644 $ci=new condition_info($cm, CONDITION_MISSING_EVERYTHING, false);
645 if ($wipefirst) {
646 $ci->wipe_conditions();
648 foreach ($fromform->conditiongradegroup as $record) {
649 if($record['conditiongradeitemid']) {
650 $ci->add_grade_condition($record['conditiongradeitemid'],
651 $record['conditiongrademin'],$record['conditiongrademax']);
654 if(isset ($fromform->conditioncompletiongroup)) {
655 foreach($fromform->conditioncompletiongroup as $record) {
656 if($record['conditionsourcecmid']) {
657 $ci->add_completion_condition($record['conditionsourcecmid'],
658 $record['conditionrequiredcompletion']);
665 * Used in course/lib.php because we need to disable the completion JS if
666 * a completion value affects a conditional activity.
668 * @global object
669 * @param object $course Moodle course object
670 * @param object $cm Moodle course-module
671 * @return bool True if this is used in a condition, false otherwise
673 public static function completion_value_used_as_condition($course, $cm) {
674 // Have we already worked out a list of required completion values
675 // for this course? If so just use that
676 global $CONDITIONLIB_PRIVATE;
677 if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) {
678 // We don't have data for this course, build it
679 $modinfo = get_fast_modinfo($course);
680 $CONDITIONLIB_PRIVATE->usedincondition[$course->id] = array();
681 foreach ($modinfo->cms as $othercm) {
682 foreach ($othercm->conditionscompletion as $cmid=>$expectedcompletion) {
683 $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
687 return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]);