|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00030 define('CONDITION_STUDENTVIEW_HIDE',0); 00033 define('CONDITION_STUDENTVIEW_SHOW',1); 00034 00036 define('CONDITION_MISSING_NOTHING',0); 00039 define('CONDITION_MISSING_EXTRATABLE',1); 00041 define('CONDITION_MISSING_EVERYTHING',2); 00042 00043 require_once($CFG->libdir.'/completionlib.php'); 00044 00049 global $CONDITIONLIB_PRIVATE; 00050 $CONDITIONLIB_PRIVATE = new stdClass; 00051 // Caches whether completion values are used in availability conditions. 00052 // Array of course => array of cmid => true. 00053 $CONDITIONLIB_PRIVATE->usedincondition = array(); 00054 00059 class condition_info { 00063 private $cm, $gotdata; 00064 00086 public function __construct($cm, $expectingmissing=CONDITION_MISSING_NOTHING, 00087 $loaddata=true) { 00088 global $DB; 00089 00090 // Check ID as otherwise we can't do the other queries 00091 if (empty($cm->id)) { 00092 throw new coding_exception("Invalid parameters; course-module ID not included"); 00093 } 00094 00095 // If not loading data, don't do anything else 00096 if (!$loaddata) { 00097 $this->cm = (object)array('id'=>$cm->id); 00098 $this->gotdata = false; 00099 return; 00100 } 00101 00102 // Missing basic data from course_modules 00103 if (!isset($cm->availablefrom) || !isset($cm->availableuntil) || 00104 !isset($cm->showavailability) || !isset($cm->course)) { 00105 if ($expectingmissing<CONDITION_MISSING_EVERYTHING) { 00106 debugging('Performance warning: condition_info constructor is 00107 faster if you pass in $cm with at least basic fields 00108 (availablefrom,availableuntil,showavailability,course). 00109 [This warning can be disabled, see phpdoc.]', 00110 DEBUG_DEVELOPER); 00111 } 00112 $cm = $DB->get_record('course_modules',array('id'=>$cm->id), 00113 'id,course,availablefrom,availableuntil,showavailability'); 00114 } 00115 00116 $this->cm = clone($cm); 00117 $this->gotdata = true; 00118 00119 // Missing extra data 00120 if (!isset($cm->conditionsgrade) || !isset($cm->conditionscompletion)) { 00121 if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) { 00122 debugging('Performance warning: condition_info constructor is 00123 faster if you pass in a $cm from get_fast_modinfo. 00124 [This warning can be disabled, see phpdoc.]', 00125 DEBUG_DEVELOPER); 00126 } 00127 00128 self::fill_availability_conditions($this->cm); 00129 } 00130 } 00131 00140 public static function fill_availability_conditions(&$cm) { 00141 if (empty($cm->id)) { 00142 throw new coding_exception("Invalid parameters; course-module ID not included"); 00143 } 00144 00145 // Does nothing if the variables are already present 00146 if (!isset($cm->conditionsgrade) || 00147 !isset($cm->conditionscompletion)) { 00148 $cm->conditionsgrade=array(); 00149 $cm->conditionscompletion=array(); 00150 00151 global $DB, $CFG; 00152 $conditions = $DB->get_records_sql($sql=" 00153 SELECT 00154 cma.id as cmaid, gi.*,cma.sourcecmid,cma.requiredcompletion,cma.gradeitemid, 00155 cma.grademin as conditiongrademin, cma.grademax as conditiongrademax 00156 FROM 00157 {course_modules_availability} cma 00158 LEFT JOIN {grade_items} gi ON gi.id=cma.gradeitemid 00159 WHERE 00160 coursemoduleid=?",array($cm->id)); 00161 foreach ($conditions as $condition) { 00162 if (!is_null($condition->sourcecmid)) { 00163 $cm->conditionscompletion[$condition->sourcecmid] = 00164 $condition->requiredcompletion; 00165 } else { 00166 $minmax = new stdClass; 00167 $minmax->min = $condition->conditiongrademin; 00168 $minmax->max = $condition->conditiongrademax; 00169 $minmax->name = self::get_grade_name($condition); 00170 $cm->conditionsgrade[$condition->gradeitemid] = $minmax; 00171 } 00172 } 00173 } 00174 } 00175 00184 private static function get_grade_name($gradeitemobj) { 00185 global $CFG; 00186 if (isset($gradeitemobj->id)) { 00187 require_once($CFG->libdir.'/gradelib.php'); 00188 $item = new grade_item; 00189 grade_object::set_properties($item, $gradeitemobj); 00190 return $item->get_name(); 00191 } else { 00192 return '!missing'; // Ooops, missing grade 00193 } 00194 } 00195 00201 public function get_full_course_module() { 00202 $this->require_data(); 00203 return $this->cm; 00204 } 00205 00213 public function add_completion_condition($cmid, $requiredcompletion) { 00214 // Add to DB 00215 global $DB; 00216 $DB->insert_record('course_modules_availability', 00217 (object)array('coursemoduleid'=>$this->cm->id, 00218 'sourcecmid'=>$cmid, 'requiredcompletion'=>$requiredcompletion), 00219 false); 00220 00221 // Store in memory too 00222 $this->cm->conditionscompletion[$cmid] = $requiredcompletion; 00223 } 00224 00236 public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) { 00237 // Normalise nulls 00238 if ($min==='') { 00239 $min = null; 00240 } 00241 if ($max==='') { 00242 $max = null; 00243 } 00244 // Add to DB 00245 global $DB; 00246 $DB->insert_record('course_modules_availability', 00247 (object)array('coursemoduleid'=>$this->cm->id, 00248 'gradeitemid'=>$gradeitemid, 'grademin'=>$min, 'grademax'=>$max), 00249 false); 00250 00251 // Store in memory too 00252 if ($updateinmemory) { 00253 $this->cm->conditionsgrade[$gradeitemid]=(object)array( 00254 'min'=>$min, 'max'=>$max); 00255 $this->cm->conditionsgrade[$gradeitemid]->name = 00256 self::get_grade_name($DB->get_record('grade_items', 00257 array('id'=>$gradeitemid))); 00258 } 00259 } 00260 00266 public function wipe_conditions() { 00267 // Wipe from DB 00268 global $DB; 00269 $DB->delete_records('course_modules_availability', 00270 array('coursemoduleid'=>$this->cm->id)); 00271 00272 // And from memory 00273 $this->cm->conditionsgrade = array(); 00274 $this->cm->conditionscompletion = array(); 00275 } 00276 00289 public function get_full_information($modinfo=null) { 00290 $this->require_data(); 00291 global $COURSE, $DB; 00292 00293 $information = ''; 00294 00295 // Completion conditions 00296 if(count($this->cm->conditionscompletion)>0) { 00297 if ($this->cm->course==$COURSE->id) { 00298 $course = $COURSE; 00299 } else { 00300 $course = $DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo'); 00301 } 00302 foreach ($this->cm->conditionscompletion as $cmid=>$expectedcompletion) { 00303 if (!$modinfo) { 00304 $modinfo = get_fast_modinfo($course); 00305 } 00306 if (empty($modinfo->cms[$cmid])) { 00307 continue; 00308 } 00309 $information .= get_string( 00310 'requires_completion_'.$expectedcompletion, 00311 'condition', $modinfo->cms[$cmid]->name).' '; 00312 } 00313 } 00314 00315 // Grade conditions 00316 if (count($this->cm->conditionsgrade)>0) { 00317 foreach ($this->cm->conditionsgrade as $gradeitemid=>$minmax) { 00318 // String depends on type of requirement. We are coy about 00319 // the actual numbers, in case grades aren't released to 00320 // students. 00321 if (is_null($minmax->min) && is_null($minmax->max)) { 00322 $string = 'any'; 00323 } else if (is_null($minmax->max)) { 00324 $string = 'min'; 00325 } else if (is_null($minmax->min)) { 00326 $string = 'max'; 00327 } else { 00328 $string = 'range'; 00329 } 00330 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' '; 00331 } 00332 } 00333 00334 // The date logic is complicated. The intention of this logic is: 00335 // 1) display date without time where possible (whenever the date is 00336 // midnight) 00337 // 2) when the 'until' date is e.g. 00:00 on the 14th, we display it as 00338 // 'until the 13th' (experience at the OU showed that students are 00339 // likely to interpret 'until <date>' as 'until the end of <date>'). 00340 // 3) This behaviour becomes confusing for 'same-day' dates where there 00341 // are some exceptions. 00342 // Users in different time zones will typically not get the 'abbreviated' 00343 // behaviour but it should work OK for them aside from that. 00344 00345 // The following cases are possible: 00346 // a) From 13:05 on 14 Oct until 12:10 on 17 Oct (exact, exact) 00347 // b) From 14 Oct until 12:11 on 17 Oct (midnight, exact) 00348 // c) From 13:05 on 14 Oct until 17 Oct (exact, midnight 18 Oct) 00349 // d) From 14 Oct until 17 Oct (midnight 14 Oct, midnight 18 Oct) 00350 // e) On 14 Oct (midnight 14 Oct, midnight 15 Oct) 00351 // f) From 13:05 on 14 Oct until 0:00 on 15 Oct (exact, midnight, same day) 00352 // g) From 0:00 on 14 Oct until 12:05 on 14 Oct (midnight, exact, same day) 00353 // h) From 13:05 on 14 Oct (exact) 00354 // i) From 14 Oct (midnight) 00355 // j) Until 13:05 on 14 Oct (exact) 00356 // k) Until 14 Oct (midnight 15 Oct) 00357 00358 // Check if start and end dates are 'midnights', if so we show in short form 00359 $shortfrom = self::is_midnight($this->cm->availablefrom); 00360 $shortuntil = self::is_midnight($this->cm->availableuntil); 00361 00362 // For some checks and for display, we need the previous day for the 'until' 00363 // value, if we are going to display it in short form 00364 if ($this->cm->availableuntil) { 00365 $daybeforeuntil = strtotime("-1 day", usergetmidnight($this->cm->availableuntil)); 00366 } 00367 00368 // Special case for if one but not both are exact and they are within a day 00369 if ($this->cm->availablefrom && $this->cm->availableuntil && 00370 $shortfrom != $shortuntil && $daybeforeuntil < $this->cm->availablefrom) { 00371 // Don't use abbreviated version (see examples f, g above) 00372 $shortfrom = false; 00373 $shortuntil = false; 00374 } 00375 00376 // When showing short end date, the display time is the 'day before' one 00377 $displayuntil = $shortuntil ? $daybeforeuntil : $this->cm->availableuntil; 00378 00379 if ($this->cm->availablefrom && $this->cm->availableuntil) { 00380 if ($shortfrom && $shortuntil && $daybeforeuntil == $this->cm->availablefrom) { 00381 $information .= get_string('requires_date_both_single_day', 'condition', 00382 self::show_time($this->cm->availablefrom, true)); 00383 } else { 00384 $information .= get_string('requires_date_both', 'condition', (object)array( 00385 'from' => self::show_time($this->cm->availablefrom, $shortfrom), 00386 'until' => self::show_time($displayuntil, $shortuntil))); 00387 } 00388 } else if ($this->cm->availablefrom) { 00389 $information .= get_string('requires_date', 'condition', 00390 self::show_time($this->cm->availablefrom, $shortfrom)); 00391 } else if ($this->cm->availableuntil) { 00392 $information .= get_string('requires_date_before', 'condition', 00393 self::show_time($displayuntil, $shortuntil)); 00394 } 00395 00396 $information = trim($information); 00397 return $information; 00398 } 00399 00407 private static function is_midnight($time) { 00408 return $time && usergetmidnight($time) == $time; 00409 } 00410 00437 public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) { 00438 $this->require_data(); 00439 global $COURSE,$DB; 00440 00441 $available = true; 00442 $information = ''; 00443 00444 // Check each completion condition 00445 if(count($this->cm->conditionscompletion)>0) { 00446 if ($this->cm->course==$COURSE->id) { 00447 $course = $COURSE; 00448 } else { 00449 $course = $DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo'); 00450 } 00451 00452 $completion = new completion_info($course); 00453 foreach ($this->cm->conditionscompletion as $cmid=>$expectedcompletion) { 00454 // If this depends on a deleted module, handle that situation 00455 // gracefully. 00456 if (!$modinfo) { 00457 $modinfo = get_fast_modinfo($course); 00458 } 00459 if (empty($modinfo->cms[$cmid])) { 00460 global $PAGE, $UNITTEST; 00461 if (!empty($UNITTEST) || (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0)) { 00462 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)"); 00463 } 00464 continue; 00465 } 00466 00467 // The completion system caches its own data 00468 $completiondata = $completion->get_data((object)array('id'=>$cmid), 00469 $grabthelot, $userid, $modinfo); 00470 00471 $thisisok = true; 00472 if ($expectedcompletion==COMPLETION_COMPLETE) { 00473 // 'Complete' also allows the pass, fail states 00474 switch ($completiondata->completionstate) { 00475 case COMPLETION_COMPLETE: 00476 case COMPLETION_COMPLETE_FAIL: 00477 case COMPLETION_COMPLETE_PASS: 00478 break; 00479 default: 00480 $thisisok = false; 00481 } 00482 } else { 00483 // Other values require exact match 00484 if ($completiondata->completionstate!=$expectedcompletion) { 00485 $thisisok = false; 00486 } 00487 } 00488 if (!$thisisok) { 00489 $available = false; 00490 $information .= get_string( 00491 'requires_completion_'.$expectedcompletion, 00492 'condition',$modinfo->cms[$cmid]->name).' '; 00493 } 00494 } 00495 } 00496 00497 // Check each grade condition 00498 if (count($this->cm->conditionsgrade)>0) { 00499 foreach ($this->cm->conditionsgrade as $gradeitemid=>$minmax) { 00500 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid); 00501 if ($score===false || 00502 (!is_null($minmax->min) && $score<$minmax->min) || 00503 (!is_null($minmax->max) && $score>=$minmax->max)) { 00504 // Grade fail 00505 $available = false; 00506 // String depends on type of requirement. We are coy about 00507 // the actual numbers, in case grades aren't released to 00508 // students. 00509 if (is_null($minmax->min) && is_null($minmax->max)) { 00510 $string = 'any'; 00511 } else if (is_null($minmax->max)) { 00512 $string = 'min'; 00513 } else if (is_null($minmax->min)) { 00514 $string = 'max'; 00515 } else { 00516 $string = 'range'; 00517 } 00518 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' '; 00519 } 00520 } 00521 } 00522 00523 // Test dates 00524 if ($this->cm->availablefrom) { 00525 if (time() < $this->cm->availablefrom) { 00526 $available = false; 00527 00528 $information .= get_string('requires_date', 'condition', 00529 self::show_time($this->cm->availablefrom, 00530 self::is_midnight($this->cm->availablefrom))); 00531 } 00532 } 00533 00534 if ($this->cm->availableuntil) { 00535 if (time() >= $this->cm->availableuntil) { 00536 $available = false; 00537 // But we don't display any information about this case. This is 00538 // because the only reason to set a 'disappear' date is usually 00539 // to get rid of outdated information/clutter in which case there 00540 // is no point in showing it... 00541 00542 // Note it would be nice if we could make it so that the 'until' 00543 // date appears below the item while the item is still accessible, 00544 // unfortunately this is not possible in the current system. Maybe 00545 // later, or if somebody else wants to add it. 00546 } 00547 } 00548 00549 $information=trim($information); 00550 return $available; 00551 } 00552 00560 private function show_time($time, $dateonly) { 00561 return userdate($time, 00562 get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig')); 00563 } 00564 00570 public function show_availability() { 00571 $this->require_data(); 00572 return $this->cm->showavailability; 00573 } 00574 00580 private function require_data() { 00581 if (!$this->gotdata) { 00582 throw new coding_exception('Error: cannot call when info was '. 00583 'constructed without data'); 00584 } 00585 } 00586 00603 private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) { 00604 global $USER, $DB, $SESSION; 00605 if ($userid==0 || $userid==$USER->id) { 00606 // For current user, go via cache in session 00607 if (empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) { 00608 $SESSION->gradescorecache = array(); 00609 $SESSION->gradescorecacheuserid = $USER->id; 00610 } 00611 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) { 00612 if ($grabthelot) { 00613 // Get all grades for the current course 00614 $rs = $DB->get_recordset_sql(" 00615 SELECT 00616 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax 00617 FROM 00618 {grade_items} gi 00619 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=? 00620 WHERE 00621 gi.courseid=?", array($USER->id, $this->cm->course)); 00622 foreach ($rs as $record) { 00623 $SESSION->gradescorecache[$record->id] = 00624 is_null($record->finalgrade) 00625 // No grade = false 00626 ? false 00627 // Otherwise convert grade to percentage 00628 : (($record->finalgrade - $record->rawgrademin) * 100) / 00629 ($record->rawgrademax - $record->rawgrademin); 00630 00631 } 00632 $rs->close(); 00633 // And if it's still not set, well it doesn't exist (eg 00634 // maybe the user set it as a condition, then deleted the 00635 // grade item) so we call it false 00636 if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) { 00637 $SESSION->gradescorecache[$gradeitemid] = false; 00638 } 00639 } else { 00640 // Just get current grade 00641 $record = $DB->get_record('grade_grades', array( 00642 'userid'=>$USER->id, 'itemid'=>$gradeitemid)); 00643 if ($record && !is_null($record->finalgrade)) { 00644 $score = (($record->finalgrade - $record->rawgrademin) * 100) / 00645 ($record->rawgrademax - $record->rawgrademin); 00646 } else { 00647 // Treat the case where row exists but is null, same as 00648 // case where row doesn't exist 00649 $score = false; 00650 } 00651 $SESSION->gradescorecache[$gradeitemid]=$score; 00652 } 00653 } 00654 return $SESSION->gradescorecache[$gradeitemid]; 00655 } else { 00656 // Not the current user, so request the score individually 00657 $record = $DB->get_record('grade_grades', array( 00658 'userid'=>$userid, 'itemid'=>$gradeitemid)); 00659 if ($record && !is_null($record->finalgrade)) { 00660 $score = (($record->finalgrade - $record->rawgrademin) * 100) / 00661 ($record->rawgrademax - $record->rawgrademin); 00662 } else { 00663 // Treat the case where row exists but is null, same as 00664 // case where row doesn't exist 00665 $score = false; 00666 } 00667 return $score; 00668 } 00669 } 00670 00676 static function wipe_session_cache() { 00677 global $SESSION; 00678 unset($SESSION->gradescorecache); 00679 unset($SESSION->gradescorecacheuserid); 00680 } 00681 00690 public static function update_cm_from_form($cm, $fromform, $wipefirst=true) { 00691 $ci=new condition_info($cm, CONDITION_MISSING_EVERYTHING, false); 00692 if ($wipefirst) { 00693 $ci->wipe_conditions(); 00694 } 00695 foreach ($fromform->conditiongradegroup as $record) { 00696 if($record['conditiongradeitemid']) { 00697 $ci->add_grade_condition($record['conditiongradeitemid'], 00698 $record['conditiongrademin'],$record['conditiongrademax']); 00699 } 00700 } 00701 if(isset ($fromform->conditioncompletiongroup)) { 00702 foreach($fromform->conditioncompletiongroup as $record) { 00703 if($record['conditionsourcecmid']) { 00704 $ci->add_completion_condition($record['conditionsourcecmid'], 00705 $record['conditionrequiredcompletion']); 00706 } 00707 } 00708 } 00709 } 00710 00720 public static function completion_value_used_as_condition($course, $cm) { 00721 // Have we already worked out a list of required completion values 00722 // for this course? If so just use that 00723 global $CONDITIONLIB_PRIVATE; 00724 if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) { 00725 // We don't have data for this course, build it 00726 $modinfo = get_fast_modinfo($course); 00727 $CONDITIONLIB_PRIVATE->usedincondition[$course->id] = array(); 00728 foreach ($modinfo->cms as $othercm) { 00729 foreach ($othercm->conditionscompletion as $cmid=>$expectedcompletion) { 00730 $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true; 00731 } 00732 } 00733 } 00734 return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]); 00735 } 00736 }