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').
22 * @subpackage 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();
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 require_once($CFG->libdir
.'/completionlib.php');
46 * @global stdClass $CONDITIONLIB_PRIVATE
47 * @name $CONDITIONLIB_PRIVATE
49 global $CONDITIONLIB_PRIVATE;
50 $CONDITIONLIB_PRIVATE = new stdClass
;
51 // Caches whether completion values are used in availability conditions.
52 // Array of course => array of cmid => true.
53 $CONDITIONLIB_PRIVATE->usedincondition
= array();
56 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
59 class condition_info
{
63 private $cm, $gotdata;
66 * Constructs with course-module details.
69 * @uses CONDITION_MISSING_NOTHING
70 * @uses CONDITION_MISSING_EVERYTHING
71 * @uses DEBUG_DEVELOPER
72 * @uses CONDITION_MISSING_EXTRATABLE
73 * @param object $cm Moodle course-module object. May have extra fields
74 * ->conditionsgrade, ->conditionscompletion which should come from
75 * get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
76 * and ->showavailability, ->course; but the only required thing is ->id.
77 * @param int $expectingmissing Used to control whether or not a developer
78 * debugging message (performance warning) will be displayed if some of
79 * the above data is missing and needs to be retrieved; a
80 * CONDITION_MISSING_xx constant
81 * @param bool $loaddata If you need a 'write-only' object, set this value
82 * to false to prevent database access from constructor
83 * @return condition_info Object which can retrieve information about the
86 public function __construct($cm, $expectingmissing=CONDITION_MISSING_NOTHING
,
90 // Check ID as otherwise we can't do the other queries
92 throw new coding_exception("Invalid parameters; course-module ID not included");
95 // If not loading data, don't do anything else
97 $this->cm
= (object)array('id'=>$cm->id
);
98 $this->gotdata
= false;
102 // Missing basic data from course_modules
103 if (!isset($cm->availablefrom
) ||
!isset($cm->availableuntil
) ||
104 !isset($cm->showavailability
) ||
!isset($cm->course
)) {
105 if ($expectingmissing<CONDITION_MISSING_EVERYTHING
) {
106 debugging('Performance warning: condition_info constructor is
107 faster if you pass in $cm with at least basic fields
108 (availablefrom,availableuntil,showavailability,course).
109 [This warning can be disabled, see phpdoc.]',
112 $cm = $DB->get_record('course_modules',array('id'=>$cm->id
),
113 'id,course,availablefrom,availableuntil,showavailability');
116 $this->cm
= clone($cm);
117 $this->gotdata
= true;
119 // Missing extra data
120 if (!isset($cm->conditionsgrade
) ||
!isset($cm->conditionscompletion
)) {
121 if ($expectingmissing<CONDITION_MISSING_EXTRATABLE
) {
122 debugging('Performance warning: condition_info constructor is
123 faster if you pass in a $cm from get_fast_modinfo.
124 [This warning can be disabled, see phpdoc.]',
128 self
::fill_availability_conditions($this->cm
);
133 * Adds the extra availability conditions (if any) into the given
134 * course-module object.
138 * @param object $cm Moodle course-module data object
140 public static function fill_availability_conditions(&$cm) {
141 if (empty($cm->id
)) {
142 throw new coding_exception("Invalid parameters; course-module ID not included");
145 // Does nothing if the variables are already present
146 if (!isset($cm->conditionsgrade
) ||
147 !isset($cm->conditionscompletion
)) {
148 $cm->conditionsgrade
=array();
149 $cm->conditionscompletion
=array();
152 $conditions = $DB->get_records_sql($sql="
154 cma.id as cmaid, gi.*,cma.sourcecmid,cma.requiredcompletion,cma.gradeitemid,
155 cma.grademin as conditiongrademin, cma.grademax as conditiongrademax
157 {course_modules_availability} cma
158 LEFT JOIN {grade_items} gi ON gi.id=cma.gradeitemid
160 coursemoduleid=?",array($cm->id
));
161 foreach ($conditions as $condition) {
162 if (!is_null($condition->sourcecmid
)) {
163 $cm->conditionscompletion
[$condition->sourcecmid
] =
164 $condition->requiredcompletion
;
166 $minmax = new stdClass
;
167 $minmax->min
= $condition->conditiongrademin
;
168 $minmax->max
= $condition->conditiongrademax
;
169 $minmax->name
= self
::get_grade_name($condition);
170 $cm->conditionsgrade
[$condition->gradeitemid
] = $minmax;
177 * Obtains the name of a grade item.
180 * @param object $gradeitemobj Object from get_record on grade_items table,
181 * (can be empty if you want to just get !missing)
182 * @return string Name of item of !missing if it didn't exist
184 private static function get_grade_name($gradeitemobj) {
186 if (isset($gradeitemobj->id
)) {
187 require_once($CFG->libdir
.'/gradelib.php');
188 $item = new grade_item
;
189 grade_object
::set_properties($item, $gradeitemobj);
190 return $item->get_name();
192 return '!missing'; // Ooops, missing grade
197 * @see require_data()
198 * @return object A course-module object with all the information required to
199 * determine availability.
201 public function get_full_course_module() {
202 $this->require_data();
207 * Adds to the database a condition based on completion of another module.
210 * @param int $cmid ID of other module
211 * @param int $requiredcompletion COMPLETION_xx constant
213 public function add_completion_condition($cmid, $requiredcompletion) {
216 $DB->insert_record('course_modules_availability',
217 (object)array('coursemoduleid'=>$this->cm
->id
,
218 'sourcecmid'=>$cmid, 'requiredcompletion'=>$requiredcompletion),
221 // Store in memory too
222 $this->cm
->conditionscompletion
[$cmid] = $requiredcompletion;
226 * Adds to the database a condition based on the value of a grade item.
229 * @param int $gradeitemid ID of grade item
230 * @param float $min Minimum grade (>=), up to 5 decimal points, or null if none
231 * @param float $max Maximum grade (<), up to 5 decimal points, or null if none
232 * @param bool $updateinmemory If true, updates data in memory; otherwise,
233 * memory version may be out of date (this has performance consequences,
234 * so don't do it unless it really needs updating)
236 public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
246 $DB->insert_record('course_modules_availability',
247 (object)array('coursemoduleid'=>$this->cm
->id
,
248 'gradeitemid'=>$gradeitemid, 'grademin'=>$min, 'grademax'=>$max),
251 // Store in memory too
252 if ($updateinmemory) {
253 $this->cm
->conditionsgrade
[$gradeitemid]=(object)array(
254 'min'=>$min, 'max'=>$max);
255 $this->cm
->conditionsgrade
[$gradeitemid]->name
=
256 self
::get_grade_name($DB->get_record('grade_items',
257 array('id'=>$gradeitemid)));
262 * Erases from the database all conditions for this activity.
266 public function wipe_conditions() {
269 $DB->delete_records('course_modules_availability',
270 array('coursemoduleid'=>$this->cm
->id
));
273 $this->cm
->conditionsgrade
= array();
274 $this->cm
->conditionscompletion
= array();
278 * Obtains a string describing all availability restrictions (even if
279 * they do not apply any more).
283 * @param object $modinfo Usually leave as null for default. Specify when
284 * calling recursively from inside get_fast_modinfo. The value supplied
285 * here must include list of all CMs with 'id' and 'name'
286 * @return string Information string (for admin) about all restrictions on
289 public function get_full_information($modinfo=null) {
290 $this->require_data();
295 // Completion conditions
296 if(count($this->cm
->conditionscompletion
)>0) {
297 if ($this->cm
->course
==$COURSE->id
) {
300 $course = $DB->get_record('course',array('id'=>$this->cm
->course
),'id,enablecompletion,modinfo');
302 foreach ($this->cm
->conditionscompletion
as $cmid=>$expectedcompletion) {
304 $modinfo = get_fast_modinfo($course);
306 if (empty($modinfo->cms
[$cmid])) {
309 $information .= get_string(
310 'requires_completion_'.$expectedcompletion,
311 'condition', $modinfo->cms
[$cmid]->name
).' ';
316 if (count($this->cm
->conditionsgrade
)>0) {
317 foreach ($this->cm
->conditionsgrade
as $gradeitemid=>$minmax) {
318 // String depends on type of requirement. We are coy about
319 // the actual numbers, in case grades aren't released to
321 if (is_null($minmax->min
) && is_null($minmax->max
)) {
323 } else if (is_null($minmax->max
)) {
325 } else if (is_null($minmax->min
)) {
330 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name
).' ';
335 if ($this->cm
->availablefrom
&& $this->cm
->availableuntil
) {
336 $information .= get_string('requires_date_both', 'condition',
338 'from' => self
::show_time($this->cm
->availablefrom
, false),
339 'until' => self
::show_time($this->cm
->availableuntil
, true)));
340 } else if ($this->cm
->availablefrom
) {
341 $information .= get_string('requires_date', 'condition',
342 self
::show_time($this->cm
->availablefrom
, false));
343 } else if ($this->cm
->availableuntil
) {
344 $information .= get_string('requires_date_before', 'condition',
345 self
::show_time($this->cm
->availableuntil
, true));
348 $information = trim($information);
353 * Determines whether this particular course-module is currently available
354 * according to these criteria.
356 * - This does not include the 'visible' setting (i.e. this might return
357 * true even if visible is false); visible is handled independently.
358 * - This does not take account of the viewhiddenactivities capability.
359 * That should apply later.
363 * @uses COMPLETION_COMPLETE
364 * @uses COMPLETION_COMPLETE_FAIL
365 * @uses COMPLETION_COMPLETE_PASS
366 * @param string $information If the item has availability restrictions,
367 * a string that describes the conditions will be stored in this variable;
368 * if this variable is set blank, that means don't display anything
369 * @param bool $grabthelot Performance hint: if true, caches information
370 * required for all course-modules, to make the front page and similar
371 * pages work more quickly (works only for current user)
372 * @param int $userid If set, specifies a different user ID to check availability for
373 * @param object $modinfo Usually leave as null for default. Specify when
374 * calling recursively from inside get_fast_modinfo. The value supplied
375 * here must include list of all CMs with 'id' and 'name'
376 * @return bool True if this item is available to the user, false otherwise
378 public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
379 $this->require_data();
385 // Check each completion condition
386 if(count($this->cm
->conditionscompletion
)>0) {
387 if ($this->cm
->course
==$COURSE->id
) {
390 $course = $DB->get_record('course',array('id'=>$this->cm
->course
),'id,enablecompletion,modinfo');
393 $completion = new completion_info($course);
394 foreach ($this->cm
->conditionscompletion
as $cmid=>$expectedcompletion) {
395 // If this depends on a deleted module, handle that situation
398 $modinfo = get_fast_modinfo($course);
400 if (empty($modinfo->cms
[$cmid])) {
401 global $PAGE, $UNITTEST;
402 if (!empty($UNITTEST) ||
(isset($PAGE) && strpos($PAGE->pagetype
, 'course-view-')===0)) {
403 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)");
408 // The completion system caches its own data
409 $completiondata = $completion->get_data((object)array('id'=>$cmid),
410 $grabthelot, $userid, $modinfo);
413 if ($expectedcompletion==COMPLETION_COMPLETE
) {
414 // 'Complete' also allows the pass, fail states
415 switch ($completiondata->completionstate
) {
416 case COMPLETION_COMPLETE
:
417 case COMPLETION_COMPLETE_FAIL
:
418 case COMPLETION_COMPLETE_PASS
:
424 // Other values require exact match
425 if ($completiondata->completionstate
!=$expectedcompletion) {
431 $information .= get_string(
432 'requires_completion_'.$expectedcompletion,
433 'condition',$modinfo->cms
[$cmid]->name
).' ';
438 // Check each grade condition
439 if (count($this->cm
->conditionsgrade
)>0) {
440 foreach ($this->cm
->conditionsgrade
as $gradeitemid=>$minmax) {
441 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
442 if ($score===false ||
443 (!is_null($minmax->min
) && $score<$minmax->min
) ||
444 (!is_null($minmax->max
) && $score>=$minmax->max
)) {
447 // String depends on type of requirement. We are coy about
448 // the actual numbers, in case grades aren't released to
450 if (is_null($minmax->min
) && is_null($minmax->max
)) {
452 } else if (is_null($minmax->max
)) {
454 } else if (is_null($minmax->min
)) {
459 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name
).' ';
465 if ($this->cm
->availablefrom
) {
466 if (time() < $this->cm
->availablefrom
) {
469 $information .= get_string('requires_date', 'condition',
470 self
::show_time($this->cm
->availablefrom
, false));
474 if ($this->cm
->availableuntil
) {
475 if (time() >= $this->cm
->availableuntil
) {
477 // But we don't display any information about this case. This is
478 // because the only reason to set a 'disappear' date is usually
479 // to get rid of outdated information/clutter in which case there
480 // is no point in showing it...
482 // Note it would be nice if we could make it so that the 'until'
483 // date appears below the item while the item is still accessible,
484 // unfortunately this is not possible in the current system. Maybe
485 // later, or if somebody else wants to add it.
489 $information=trim($information);
494 * Shows a time either as a date (if it falls exactly on the day) or
495 * a full date and time, according to user's timezone.
497 * @param int $time Time
498 * @param bool $until True if this date should be treated as the second of
499 * an inclusive pair - if so the time will be shown unless date is 23:59:59.
500 * Without this the date shows for 0:00:00.
501 * @return string Date
503 private function show_time($time, $until) {
504 // Break down the time into fields
505 $userdate = usergetdate($time);
507 // Handle the 'inclusive' second date
509 $dateonly = $userdate['hours']==23 && $userdate['minutes']==59 &&
510 $userdate['seconds']==59;
512 $dateonly = $userdate['hours']==0 && $userdate['minutes']==0 &&
513 $userdate['seconds']==0;
516 return userdate($time, get_string(
517 $dateonly ?
'strftimedate' : 'strftimedatetime', 'langconfig'));
521 * @return bool True if information about availability should be shown to
523 * @throws coding_exception If data wasn't loaded
525 public function show_availability() {
526 $this->require_data();
527 return $this->cm
->showavailability
;
531 * Internal function cheks that data was loaded.
533 * @return void throws coding_exception If data wasn't loaded
535 private function require_data() {
536 if (!$this->gotdata
) {
537 throw new coding_exception('Error: cannot call when info was '.
538 'constructed without data');
543 * Obtains a grade score. Note that this score should not be displayed to
544 * the user, because gradebook rules might prohibit that. It may be a
545 * non-final score subject to adjustment later.
550 * @param int $gradeitemid Grade item ID we're interested in
551 * @param bool $grabthelot If true, grabs all scores for current user on
552 * this course, so that later ones come from cache
553 * @param int $userid Set if requesting grade for a different user (does
555 * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
556 * or 37.21), or false if user does not have a grade yet
558 private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
559 global $USER, $DB, $SESSION;
560 if ($userid==0 ||
$userid==$USER->id
) {
561 // For current user, go via cache in session
562 if (empty($SESSION->gradescorecache
) ||
$SESSION->gradescorecacheuserid
!=$USER->id
) {
563 $SESSION->gradescorecache
= array();
564 $SESSION->gradescorecacheuserid
= $USER->id
;
566 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache
)) {
568 // Get all grades for the current course
569 $rs = $DB->get_recordset_sql("
571 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
574 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
576 gi.courseid=?", array($USER->id
, $this->cm
->course
));
577 foreach ($rs as $record) {
578 $SESSION->gradescorecache
[$record->id
] =
579 is_null($record->finalgrade
)
582 // Otherwise convert grade to percentage
583 : (($record->finalgrade
- $record->rawgrademin
) * 100) /
584 ($record->rawgrademax
- $record->rawgrademin
);
588 // And if it's still not set, well it doesn't exist (eg
589 // maybe the user set it as a condition, then deleted the
590 // grade item) so we call it false
591 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache
)) {
592 $SESSION->gradescorecache
[$gradeitemid] = false;
595 // Just get current grade
596 $record = $DB->get_record('grade_grades', array(
597 'userid'=>$USER->id
, 'itemid'=>$gradeitemid));
598 if ($record && !is_null($record->finalgrade
)) {
599 $score = (($record->finalgrade
- $record->rawgrademin
) * 100) /
600 ($record->rawgrademax
- $record->rawgrademin
);
602 // Treat the case where row exists but is null, same as
603 // case where row doesn't exist
606 $SESSION->gradescorecache
[$gradeitemid]=$score;
609 return $SESSION->gradescorecache
[$gradeitemid];
611 // Not the current user, so request the score individually
612 $record = $DB->get_record('grade_grades', array(
613 'userid'=>$userid, 'itemid'=>$gradeitemid));
614 if ($record && !is_null($record->finalgrade
)) {
615 $score = (($record->finalgrade
- $record->rawgrademin
) * 100) /
616 ($record->rawgrademax
- $record->rawgrademin
);
618 // Treat the case where row exists but is null, same as
619 // case where row doesn't exist
627 * For testing only. Wipes information cached in user session.
631 static function wipe_session_cache() {
633 unset($SESSION->gradescorecache
);
634 unset($SESSION->gradescorecacheuserid
);
638 * Utility function called by modedit.php; updates the
639 * course_modules_availability table based on the module form data.
641 * @param object $cm Course-module with as much data as necessary, min id
642 * @param object $fromform
643 * @param bool $wipefirst Defaults to true
645 public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
646 $ci=new condition_info($cm, CONDITION_MISSING_EVERYTHING
, false);
648 $ci->wipe_conditions();
650 foreach ($fromform->conditiongradegroup
as $record) {
651 if($record['conditiongradeitemid']) {
652 $ci->add_grade_condition($record['conditiongradeitemid'],
653 $record['conditiongrademin'],$record['conditiongrademax']);
656 if(isset ($fromform->conditioncompletiongroup
)) {
657 foreach($fromform->conditioncompletiongroup
as $record) {
658 if($record['conditionsourcecmid']) {
659 $ci->add_completion_condition($record['conditionsourcecmid'],
660 $record['conditionrequiredcompletion']);
667 * Used in course/lib.php because we need to disable the completion JS if
668 * a completion value affects a conditional activity.
671 * @param object $course Moodle course object
672 * @param object $cm Moodle course-module
673 * @return bool True if this is used in a condition, false otherwise
675 public static function completion_value_used_as_condition($course, $cm) {
676 // Have we already worked out a list of required completion values
677 // for this course? If so just use that
678 global $CONDITIONLIB_PRIVATE;
679 if (!array_key_exists($course->id
, $CONDITIONLIB_PRIVATE->usedincondition
)) {
680 // We don't have data for this course, build it
681 $modinfo = get_fast_modinfo($course);
682 $CONDITIONLIB_PRIVATE->usedincondition
[$course->id
] = array();
683 foreach ($modinfo->cms
as $othercm) {
684 foreach ($othercm->conditionscompletion
as $cmid=>$expectedcompletion) {
685 $CONDITIONLIB_PRIVATE->usedincondition
[$course->id
][$cmid] = true;
689 return array_key_exists($cm->id
, $CONDITIONLIB_PRIVATE->usedincondition
[$course->id
]);