PageRenderTime 64ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/gradelib.php

https://github.com/mylescarrick/moodle
PHP | 1366 lines | 841 code | 206 blank | 319 comment | 198 complexity | 6a9dca8110e3197a7c9681bb955995e1 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, BSD-3-Clause
  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.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Library of functions for gradebook - both public and internal
  18. *
  19. * @package core
  20. * @subpackage grade
  21. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. /** Include essential files */
  26. require_once($CFG->libdir . '/grade/constants.php');
  27. require_once($CFG->libdir . '/grade/grade_category.php');
  28. require_once($CFG->libdir . '/grade/grade_item.php');
  29. require_once($CFG->libdir . '/grade/grade_grade.php');
  30. require_once($CFG->libdir . '/grade/grade_scale.php');
  31. require_once($CFG->libdir . '/grade/grade_outcome.php');
  32. /////////////////////////////////////////////////////////////////////
  33. ///// Start of public API for communication with modules/blocks /////
  34. /////////////////////////////////////////////////////////////////////
  35. /**
  36. * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
  37. * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded', missing property
  38. * or key means do not change existing.
  39. *
  40. * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
  41. * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
  42. *
  43. * Manual, course or category items can not be updated by this function.
  44. * @access public
  45. * @global object
  46. * @global object
  47. * @global object
  48. * @param string $source source of the grade such as 'mod/assignment'
  49. * @param int $courseid id of course
  50. * @param string $itemtype type of grade item - mod, block
  51. * @param string $itemmodule more specific then $itemtype - assignment, forum, etc.; maybe NULL for some item types
  52. * @param int $iteminstance instance it of graded subject
  53. * @param int $itemnumber most probably 0, modules can use other numbers when having more than one grades for each user
  54. * @param mixed $grades grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
  55. * @param mixed $itemdetails object or array describing the grading item, NULL if no change
  56. */
  57. function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
  58. global $USER, $CFG, $DB;
  59. // only following grade_item properties can be changed in this function
  60. $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
  61. // list of 10,5 numeric fields
  62. $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
  63. // grade item identification
  64. $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
  65. if (is_null($courseid) or is_null($itemtype)) {
  66. debugging('Missing courseid or itemtype');
  67. return GRADE_UPDATE_FAILED;
  68. }
  69. if (!$grade_items = grade_item::fetch_all($params)) {
  70. // create a new one
  71. $grade_item = false;
  72. } else if (count($grade_items) == 1){
  73. $grade_item = reset($grade_items);
  74. unset($grade_items); //release memory
  75. } else {
  76. debugging('Found more than one grade item');
  77. return GRADE_UPDATE_MULTIPLE;
  78. }
  79. if (!empty($itemdetails['deleted'])) {
  80. if ($grade_item) {
  81. if ($grade_item->delete($source)) {
  82. return GRADE_UPDATE_OK;
  83. } else {
  84. return GRADE_UPDATE_FAILED;
  85. }
  86. }
  87. return GRADE_UPDATE_OK;
  88. }
  89. /// Create or update the grade_item if needed
  90. if (!$grade_item) {
  91. if ($itemdetails) {
  92. $itemdetails = (array)$itemdetails;
  93. // grademin and grademax ignored when scale specified
  94. if (array_key_exists('scaleid', $itemdetails)) {
  95. if ($itemdetails['scaleid']) {
  96. unset($itemdetails['grademin']);
  97. unset($itemdetails['grademax']);
  98. }
  99. }
  100. foreach ($itemdetails as $k=>$v) {
  101. if (!in_array($k, $allowed)) {
  102. // ignore it
  103. continue;
  104. }
  105. if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
  106. // no grade item needed!
  107. return GRADE_UPDATE_OK;
  108. }
  109. $params[$k] = $v;
  110. }
  111. }
  112. $grade_item = new grade_item($params);
  113. $grade_item->insert();
  114. } else {
  115. if ($grade_item->is_locked()) {
  116. // no notice() here, test returned value instead!
  117. return GRADE_UPDATE_ITEM_LOCKED;
  118. }
  119. if ($itemdetails) {
  120. $itemdetails = (array)$itemdetails;
  121. $update = false;
  122. foreach ($itemdetails as $k=>$v) {
  123. if (!in_array($k, $allowed)) {
  124. // ignore it
  125. continue;
  126. }
  127. if (in_array($k, $floats)) {
  128. if (grade_floats_different($grade_item->{$k}, $v)) {
  129. $grade_item->{$k} = $v;
  130. $update = true;
  131. }
  132. } else {
  133. if ($grade_item->{$k} != $v) {
  134. $grade_item->{$k} = $v;
  135. $update = true;
  136. }
  137. }
  138. }
  139. if ($update) {
  140. $grade_item->update();
  141. }
  142. }
  143. }
  144. /// reset grades if requested
  145. if (!empty($itemdetails['reset'])) {
  146. $grade_item->delete_all_grades('reset');
  147. return GRADE_UPDATE_OK;
  148. }
  149. /// Some extra checks
  150. // do we use grading?
  151. if ($grade_item->gradetype == GRADE_TYPE_NONE) {
  152. return GRADE_UPDATE_OK;
  153. }
  154. // no grade submitted
  155. if (empty($grades)) {
  156. return GRADE_UPDATE_OK;
  157. }
  158. /// Finally start processing of grades
  159. if (is_object($grades)) {
  160. $grades = array($grades->userid=>$grades);
  161. } else {
  162. if (array_key_exists('userid', $grades)) {
  163. $grades = array($grades['userid']=>$grades);
  164. }
  165. }
  166. /// normalize and verify grade array
  167. foreach($grades as $k=>$g) {
  168. if (!is_array($g)) {
  169. $g = (array)$g;
  170. $grades[$k] = $g;
  171. }
  172. if (empty($g['userid']) or $k != $g['userid']) {
  173. debugging('Incorrect grade array index, must be user id! Grade ignored.');
  174. unset($grades[$k]);
  175. }
  176. }
  177. if (empty($grades)) {
  178. return GRADE_UPDATE_FAILED;
  179. }
  180. $count = count($grades);
  181. if ($count > 0 and $count < 200) {
  182. list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid0');
  183. $params['gid'] = $grade_item->id;
  184. $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
  185. } else {
  186. $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
  187. $params = array('gid'=>$grade_item->id);
  188. }
  189. $rs = $DB->get_recordset_sql($sql, $params);
  190. $failed = false;
  191. while (count($grades) > 0) {
  192. $grade_grade = null;
  193. $grade = null;
  194. foreach ($rs as $gd) {
  195. $userid = $gd->userid;
  196. if (!isset($grades[$userid])) {
  197. // this grade not requested, continue
  198. continue;
  199. }
  200. // existing grade requested
  201. $grade = $grades[$userid];
  202. $grade_grade = new grade_grade($gd, false);
  203. unset($grades[$userid]);
  204. break;
  205. }
  206. if (is_null($grade_grade)) {
  207. if (count($grades) == 0) {
  208. // no more grades to process
  209. break;
  210. }
  211. $grade = reset($grades);
  212. $userid = $grade['userid'];
  213. $grade_grade = new grade_grade(array('itemid'=>$grade_item->id, 'userid'=>$userid), false);
  214. $grade_grade->load_optional_fields(); // add feedback and info too
  215. unset($grades[$userid]);
  216. }
  217. $rawgrade = false;
  218. $feedback = false;
  219. $feedbackformat = FORMAT_MOODLE;
  220. $usermodified = $USER->id;
  221. $datesubmitted = null;
  222. $dategraded = null;
  223. if (array_key_exists('rawgrade', $grade)) {
  224. $rawgrade = $grade['rawgrade'];
  225. }
  226. if (array_key_exists('feedback', $grade)) {
  227. $feedback = $grade['feedback'];
  228. }
  229. if (array_key_exists('feedbackformat', $grade)) {
  230. $feedbackformat = $grade['feedbackformat'];
  231. }
  232. if (array_key_exists('usermodified', $grade)) {
  233. $usermodified = $grade['usermodified'];
  234. }
  235. if (array_key_exists('datesubmitted', $grade)) {
  236. $datesubmitted = $grade['datesubmitted'];
  237. }
  238. if (array_key_exists('dategraded', $grade)) {
  239. $dategraded = $grade['dategraded'];
  240. }
  241. // update or insert the grade
  242. if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) {
  243. $failed = true;
  244. }
  245. }
  246. if ($rs) {
  247. $rs->close();
  248. }
  249. if (!$failed) {
  250. return GRADE_UPDATE_OK;
  251. } else {
  252. return GRADE_UPDATE_FAILED;
  253. }
  254. }
  255. /**
  256. * Updates outcomes of user
  257. * Manual outcomes can not be updated.
  258. *
  259. * @access public
  260. * @param string $source source of the grade such as 'mod/assignment'
  261. * @param int $courseid id of course
  262. * @param string $itemtype 'mod', 'block'
  263. * @param string $itemmodule 'forum, 'quiz', etc.
  264. * @param int $iteminstance id of the item module
  265. * @param int $userid ID of the graded user
  266. * @param array $data array itemnumber=>outcomegrade
  267. * @return boolean returns true if grade items were found and updated successfully
  268. */
  269. function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
  270. if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
  271. $result = true;
  272. foreach ($items as $item) {
  273. if (!array_key_exists($item->itemnumber, $data)) {
  274. continue;
  275. }
  276. $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
  277. $result = ($item->update_final_grade($userid, $grade, $source) && $result);
  278. }
  279. return $result;
  280. }
  281. return false; //grade items not found
  282. }
  283. /**
  284. * Returns grading information for given activity - optionally with users grades
  285. * Manual, course or category items can not be queried.
  286. *
  287. * @access public
  288. * @global object
  289. * @param int $courseid id of course
  290. * @param string $itemtype 'mod', 'block'
  291. * @param string $itemmodule 'forum, 'quiz', etc.
  292. * @param int $iteminstance id of the item module
  293. * @param array|int $userid_or_ids optional id of the graded user or array of ids; if userid not used, returns only information about grade_item
  294. * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
  295. */
  296. function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
  297. global $CFG;
  298. $return = new stdClass();
  299. $return->items = array();
  300. $return->outcomes = array();
  301. $course_item = grade_item::fetch_course_item($courseid);
  302. $needsupdate = array();
  303. if ($course_item->needsupdate) {
  304. $result = grade_regrade_final_grades($courseid);
  305. if ($result !== true) {
  306. $needsupdate = array_keys($result);
  307. }
  308. }
  309. if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
  310. foreach ($grade_items as $grade_item) {
  311. $decimalpoints = null;
  312. if (empty($grade_item->outcomeid)) {
  313. // prepare information about grade item
  314. $item = new stdClass();
  315. $item->itemnumber = $grade_item->itemnumber;
  316. $item->scaleid = $grade_item->scaleid;
  317. $item->name = $grade_item->get_name();
  318. $item->grademin = $grade_item->grademin;
  319. $item->grademax = $grade_item->grademax;
  320. $item->gradepass = $grade_item->gradepass;
  321. $item->locked = $grade_item->is_locked();
  322. $item->hidden = $grade_item->is_hidden();
  323. $item->grades = array();
  324. switch ($grade_item->gradetype) {
  325. case GRADE_TYPE_NONE:
  326. continue;
  327. case GRADE_TYPE_VALUE:
  328. $item->scaleid = 0;
  329. break;
  330. case GRADE_TYPE_TEXT:
  331. $item->scaleid = 0;
  332. $item->grademin = 0;
  333. $item->grademax = 0;
  334. $item->gradepass = 0;
  335. break;
  336. }
  337. if (empty($userid_or_ids)) {
  338. $userids = array();
  339. } else if (is_array($userid_or_ids)) {
  340. $userids = $userid_or_ids;
  341. } else {
  342. $userids = array($userid_or_ids);
  343. }
  344. if ($userids) {
  345. $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
  346. foreach ($userids as $userid) {
  347. $grade_grades[$userid]->grade_item =& $grade_item;
  348. $grade = new stdClass();
  349. $grade->grade = $grade_grades[$userid]->finalgrade;
  350. $grade->locked = $grade_grades[$userid]->is_locked();
  351. $grade->hidden = $grade_grades[$userid]->is_hidden();
  352. $grade->overridden = $grade_grades[$userid]->overridden;
  353. $grade->feedback = $grade_grades[$userid]->feedback;
  354. $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
  355. $grade->usermodified = $grade_grades[$userid]->usermodified;
  356. $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
  357. $grade->dategraded = $grade_grades[$userid]->get_dategraded();
  358. // create text representation of grade
  359. if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
  360. $grade->grade = null;
  361. $grade->str_grade = '-';
  362. $grade->str_long_grade = $grade->str_grade;
  363. } else if (in_array($grade_item->id, $needsupdate)) {
  364. $grade->grade = false;
  365. $grade->str_grade = get_string('error');
  366. $grade->str_long_grade = $grade->str_grade;
  367. } else if (is_null($grade->grade)) {
  368. $grade->str_grade = '-';
  369. $grade->str_long_grade = $grade->str_grade;
  370. } else {
  371. $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
  372. if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
  373. $grade->str_long_grade = $grade->str_grade;
  374. } else {
  375. $a = new stdClass();
  376. $a->grade = $grade->str_grade;
  377. $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
  378. $grade->str_long_grade = get_string('gradelong', 'grades', $a);
  379. }
  380. }
  381. // create html representation of feedback
  382. if (is_null($grade->feedback)) {
  383. $grade->str_feedback = '';
  384. } else {
  385. $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
  386. }
  387. $item->grades[$userid] = $grade;
  388. }
  389. }
  390. $return->items[$grade_item->itemnumber] = $item;
  391. } else {
  392. if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
  393. debugging('Incorect outcomeid found');
  394. continue;
  395. }
  396. // outcome info
  397. $outcome = new stdClass();
  398. $outcome->itemnumber = $grade_item->itemnumber;
  399. $outcome->scaleid = $grade_outcome->scaleid;
  400. $outcome->name = $grade_outcome->get_name();
  401. $outcome->locked = $grade_item->is_locked();
  402. $outcome->hidden = $grade_item->is_hidden();
  403. if (empty($userid_or_ids)) {
  404. $userids = array();
  405. } else if (is_array($userid_or_ids)) {
  406. $userids = $userid_or_ids;
  407. } else {
  408. $userids = array($userid_or_ids);
  409. }
  410. if ($userids) {
  411. $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
  412. foreach ($userids as $userid) {
  413. $grade_grades[$userid]->grade_item =& $grade_item;
  414. $grade = new stdClass();
  415. $grade->grade = $grade_grades[$userid]->finalgrade;
  416. $grade->locked = $grade_grades[$userid]->is_locked();
  417. $grade->hidden = $grade_grades[$userid]->is_hidden();
  418. $grade->feedback = $grade_grades[$userid]->feedback;
  419. $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
  420. $grade->usermodified = $grade_grades[$userid]->usermodified;
  421. // create text representation of grade
  422. if (in_array($grade_item->id, $needsupdate)) {
  423. $grade->grade = false;
  424. $grade->str_grade = get_string('error');
  425. } else if (is_null($grade->grade)) {
  426. $grade->grade = 0;
  427. $grade->str_grade = get_string('nooutcome', 'grades');
  428. } else {
  429. $grade->grade = (int)$grade->grade;
  430. $scale = $grade_item->load_scale();
  431. $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
  432. }
  433. // create html representation of feedback
  434. if (is_null($grade->feedback)) {
  435. $grade->str_feedback = '';
  436. } else {
  437. $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
  438. }
  439. $outcome->grades[$userid] = $grade;
  440. }
  441. }
  442. if (isset($return->outcomes[$grade_item->itemnumber])) {
  443. // itemnumber duplicates - lets fix them!
  444. $newnumber = $grade_item->itemnumber + 1;
  445. while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
  446. $newnumber++;
  447. }
  448. $outcome->itemnumber = $newnumber;
  449. $grade_item->itemnumber = $newnumber;
  450. $grade_item->update('system');
  451. }
  452. $return->outcomes[$grade_item->itemnumber] = $outcome;
  453. }
  454. }
  455. }
  456. // sort results using itemnumbers
  457. ksort($return->items, SORT_NUMERIC);
  458. ksort($return->outcomes, SORT_NUMERIC);
  459. return $return;
  460. }
  461. ///////////////////////////////////////////////////////////////////
  462. ///// End of public API for communication with modules/blocks /////
  463. ///////////////////////////////////////////////////////////////////
  464. ///////////////////////////////////////////////////////////////////
  465. ///// Internal API: used by gradebook plugins and Moodle core /////
  466. ///////////////////////////////////////////////////////////////////
  467. /**
  468. * Returns course gradebook setting
  469. *
  470. * @global object
  471. * @param int $courseid
  472. * @param string $name of setting, maybe null if reset only
  473. * @param string $default
  474. * @param bool $resetcache force reset of internal static cache
  475. * @return string value, NULL if no setting
  476. */
  477. function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
  478. global $DB;
  479. static $cache = array();
  480. if ($resetcache or !array_key_exists($courseid, $cache)) {
  481. $cache[$courseid] = array();
  482. } else if (is_null($name)) {
  483. return null;
  484. } else if (array_key_exists($name, $cache[$courseid])) {
  485. return $cache[$courseid][$name];
  486. }
  487. if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
  488. $result = null;
  489. } else {
  490. $result = $data->value;
  491. }
  492. if (is_null($result)) {
  493. $result = $default;
  494. }
  495. $cache[$courseid][$name] = $result;
  496. return $result;
  497. }
  498. /**
  499. * Returns all course gradebook settings as object properties
  500. *
  501. * @global object
  502. * @param int $courseid
  503. * @return object
  504. */
  505. function grade_get_settings($courseid) {
  506. global $DB;
  507. $settings = new stdClass();
  508. $settings->id = $courseid;
  509. if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
  510. foreach ($records as $record) {
  511. $settings->{$record->name} = $record->value;
  512. }
  513. }
  514. return $settings;
  515. }
  516. /**
  517. * Add/update course gradebook setting
  518. *
  519. * @global object
  520. * @param int $courseid
  521. * @param string $name of setting
  522. * @param string value, NULL means no setting==remove
  523. * @return void
  524. */
  525. function grade_set_setting($courseid, $name, $value) {
  526. global $DB;
  527. if (is_null($value)) {
  528. $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
  529. } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
  530. $data = new stdClass();
  531. $data->courseid = $courseid;
  532. $data->name = $name;
  533. $data->value = $value;
  534. $DB->insert_record('grade_settings', $data);
  535. } else {
  536. $data = new stdClass();
  537. $data->id = $existing->id;
  538. $data->value = $value;
  539. $DB->update_record('grade_settings', $data);
  540. }
  541. grade_get_setting($courseid, null, null, true); // reset the cache
  542. }
  543. /**
  544. * Returns string representation of grade value
  545. *
  546. * @param float $value grade value
  547. * @param object $grade_item - by reference to prevent scale reloading
  548. * @param bool $localized use localised decimal separator
  549. * @param int $displaytype type of display - GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
  550. * @param int $decimals number of decimal places when displaying float values
  551. * @return string
  552. */
  553. function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
  554. if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
  555. return '';
  556. }
  557. // no grade yet?
  558. if (is_null($value)) {
  559. return '-';
  560. }
  561. if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
  562. //unknown type??
  563. return '';
  564. }
  565. if (is_null($displaytype)) {
  566. $displaytype = $grade_item->get_displaytype();
  567. }
  568. if (is_null($decimals)) {
  569. $decimals = $grade_item->get_decimals();
  570. }
  571. switch ($displaytype) {
  572. case GRADE_DISPLAY_TYPE_REAL:
  573. return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
  574. case GRADE_DISPLAY_TYPE_PERCENTAGE:
  575. return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
  576. case GRADE_DISPLAY_TYPE_LETTER:
  577. return grade_format_gradevalue_letter($value, $grade_item);
  578. case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
  579. return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
  580. grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
  581. case GRADE_DISPLAY_TYPE_REAL_LETTER:
  582. return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
  583. grade_format_gradevalue_letter($value, $grade_item) . ')';
  584. case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
  585. return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
  586. grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
  587. case GRADE_DISPLAY_TYPE_LETTER_REAL:
  588. return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
  589. grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
  590. case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
  591. return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
  592. grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
  593. case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
  594. return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
  595. grade_format_gradevalue_letter($value, $grade_item) . ')';
  596. default:
  597. return '';
  598. }
  599. }
  600. /**
  601. * @todo Document this function
  602. */
  603. function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
  604. if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
  605. if (!$scale = $grade_item->load_scale()) {
  606. return get_string('error');
  607. }
  608. $value = $grade_item->bounded_grade($value);
  609. return format_string($scale->scale_items[$value-1]);
  610. } else {
  611. return format_float($value, $decimals, $localized);
  612. }
  613. }
  614. /**
  615. * @todo Document this function
  616. */
  617. function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
  618. $min = $grade_item->grademin;
  619. $max = $grade_item->grademax;
  620. if ($min == $max) {
  621. return '';
  622. }
  623. $value = $grade_item->bounded_grade($value);
  624. $percentage = (($value-$min)*100)/($max-$min);
  625. return format_float($percentage, $decimals, $localized).' %';
  626. }
  627. /**
  628. * @todo Document this function
  629. */
  630. function grade_format_gradevalue_letter($value, $grade_item) {
  631. $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid);
  632. if (!$letters = grade_get_letters($context)) {
  633. return ''; // no letters??
  634. }
  635. if (is_null($value)) {
  636. return '-';
  637. }
  638. $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
  639. $value = bounded_number(0, $value, 100); // just in case
  640. foreach ($letters as $boundary => $letter) {
  641. if ($value >= $boundary) {
  642. return format_string($letter);
  643. }
  644. }
  645. return '-'; // no match? maybe '' would be more correct
  646. }
  647. /**
  648. * Returns grade options for gradebook category menu
  649. *
  650. * @param int $courseid
  651. * @param bool $includenew include option for new category (-1)
  652. * @return array of grade categories in course
  653. */
  654. function grade_get_categories_menu($courseid, $includenew=false) {
  655. $result = array();
  656. if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
  657. //make sure course category exists
  658. if (!grade_category::fetch_course_category($courseid)) {
  659. debugging('Can not create course grade category!');
  660. return $result;
  661. }
  662. $categories = grade_category::fetch_all(array('courseid'=>$courseid));
  663. }
  664. foreach ($categories as $key=>$category) {
  665. if ($category->is_course_category()) {
  666. $result[$category->id] = get_string('uncategorised', 'grades');
  667. unset($categories[$key]);
  668. }
  669. }
  670. if ($includenew) {
  671. $result[-1] = get_string('newcategory', 'grades');
  672. }
  673. $cats = array();
  674. foreach ($categories as $category) {
  675. $cats[$category->id] = $category->get_name();
  676. }
  677. textlib_get_instance()->asort($cats);
  678. return ($result+$cats);
  679. }
  680. /**
  681. * Returns grade letters array used in context
  682. *
  683. * @param object $context object or null for defaults
  684. * @return array of grade_boundary=>letter_string
  685. */
  686. function grade_get_letters($context=null) {
  687. global $DB;
  688. if (empty($context)) {
  689. //default grading letters
  690. return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
  691. }
  692. static $cache = array();
  693. if (array_key_exists($context->id, $cache)) {
  694. return $cache[$context->id];
  695. }
  696. if (count($cache) > 100) {
  697. $cache = array(); // cache size limit
  698. }
  699. $letters = array();
  700. $contexts = get_parent_contexts($context);
  701. array_unshift($contexts, $context->id);
  702. foreach ($contexts as $ctxid) {
  703. if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
  704. foreach ($records as $record) {
  705. $letters[$record->lowerboundary] = $record->letter;
  706. }
  707. }
  708. if (!empty($letters)) {
  709. $cache[$context->id] = $letters;
  710. return $letters;
  711. }
  712. }
  713. $letters = grade_get_letters(null);
  714. $cache[$context->id] = $letters;
  715. return $letters;
  716. }
  717. /**
  718. * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact
  719. *
  720. * @global object
  721. * @param string idnumber string (with magic quotes)
  722. * @param int $courseid id numbers are course unique only
  723. * @param object $grade_item is item idnumber
  724. * @param object $cm used for course module idnumbers and items attached to modules
  725. * @return boolean true means idnumber ok
  726. */
  727. function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
  728. global $DB;
  729. if ($idnumber == '') {
  730. //we allow empty idnumbers
  731. return true;
  732. }
  733. // keep existing even when not unique
  734. if ($cm and $cm->idnumber == $idnumber) {
  735. if ($grade_item and $grade_item->itemnumber != 0) {
  736. // grade item with itemnumber > 0 can't have the same idnumber as the main
  737. // itemnumber 0 which is synced with course_modules
  738. return false;
  739. }
  740. return true;
  741. } else if ($grade_item and $grade_item->idnumber == $idnumber) {
  742. return true;
  743. }
  744. if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
  745. return false;
  746. }
  747. if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
  748. return false;
  749. }
  750. return true;
  751. }
  752. /**
  753. * Force final grade recalculation in all course items
  754. *
  755. * @global object
  756. * @param int $courseid
  757. */
  758. function grade_force_full_regrading($courseid) {
  759. global $DB;
  760. $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
  761. }
  762. /**
  763. * Forces regrading of all site grades - usualy when chanign site setings
  764. * @global object
  765. * @global object
  766. */
  767. function grade_force_site_regrading() {
  768. global $CFG, $DB;
  769. $DB->set_field('grade_items', 'needsupdate', 1);
  770. }
  771. /**
  772. * Updates all final grades in course.
  773. *
  774. * @param int $courseid
  775. * @param int $userid if specified, try to do a quick regrading of grades of this user only
  776. * @param object $updated_item the item in which
  777. * @return boolean true if ok, array of errors if problems found (item id is used as key)
  778. */
  779. function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
  780. $course_item = grade_item::fetch_course_item($courseid);
  781. if ($userid) {
  782. // one raw grade updated for one user
  783. if (empty($updated_item)) {
  784. print_error("cannotbenull", 'debug', '', "updated_item");
  785. }
  786. if ($course_item->needsupdate) {
  787. $updated_item->force_regrading();
  788. return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
  789. }
  790. } else {
  791. if (!$course_item->needsupdate) {
  792. // nothing to do :-)
  793. return true;
  794. }
  795. }
  796. $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
  797. $depends_on = array();
  798. // first mark all category and calculated items as needing regrading
  799. // this is slower, but 100% accurate
  800. foreach ($grade_items as $gid=>$gitem) {
  801. if (!empty($updated_item) and $updated_item->id == $gid) {
  802. $grade_items[$gid]->needsupdate = 1;
  803. } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
  804. $grade_items[$gid]->needsupdate = 1;
  805. }
  806. // construct depends_on lookup array
  807. $depends_on[$gid] = $grade_items[$gid]->depends_on();
  808. }
  809. $errors = array();
  810. $finalids = array();
  811. $gids = array_keys($grade_items);
  812. $failed = 0;
  813. while (count($finalids) < count($gids)) { // work until all grades are final or error found
  814. $count = 0;
  815. foreach ($gids as $gid) {
  816. if (in_array($gid, $finalids)) {
  817. continue; // already final
  818. }
  819. if (!$grade_items[$gid]->needsupdate) {
  820. $finalids[] = $gid; // we can make it final - does not need update
  821. continue;
  822. }
  823. $doupdate = true;
  824. foreach ($depends_on[$gid] as $did) {
  825. if (!in_array($did, $finalids)) {
  826. $doupdate = false;
  827. continue; // this item depends on something that is not yet in finals array
  828. }
  829. }
  830. //oki - let's update, calculate or aggregate :-)
  831. if ($doupdate) {
  832. $result = $grade_items[$gid]->regrade_final_grades($userid);
  833. if ($result === true) {
  834. $grade_items[$gid]->regrading_finished();
  835. $grade_items[$gid]->check_locktime(); // do the locktime item locking
  836. $count++;
  837. $finalids[] = $gid;
  838. } else {
  839. $grade_items[$gid]->force_regrading();
  840. $errors[$gid] = $result;
  841. }
  842. }
  843. }
  844. if ($count == 0) {
  845. $failed++;
  846. } else {
  847. $failed = 0;
  848. }
  849. if ($failed > 1) {
  850. foreach($gids as $gid) {
  851. if (in_array($gid, $finalids)) {
  852. continue; // this one is ok
  853. }
  854. $grade_items[$gid]->force_regrading();
  855. $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
  856. }
  857. break; // oki, found error
  858. }
  859. }
  860. if (count($errors) == 0) {
  861. if (empty($userid)) {
  862. // do the locktime locking of grades, but only when doing full regrading
  863. grade_grade::check_locktime_all($gids);
  864. }
  865. return true;
  866. } else {
  867. return $errors;
  868. }
  869. }
  870. /**
  871. * Refetches data from all course activities
  872. *
  873. * @global object
  874. * @global object
  875. * @param int $courseid
  876. * @param string $modname
  877. * @return void
  878. */
  879. function grade_grab_course_grades($courseid, $modname=null) {
  880. global $CFG, $DB;
  881. if ($modname) {
  882. $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
  883. FROM {".$modname."} a, {course_modules} cm, {modules} m
  884. WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
  885. $params = array('modname'=>$modname, 'courseid'=>$courseid);
  886. if ($modinstances = $DB->get_records_sql($sql, $params)) {
  887. foreach ($modinstances as $modinstance) {
  888. grade_update_mod_grades($modinstance);
  889. }
  890. }
  891. return;
  892. }
  893. if (!$mods = get_plugin_list('mod') ) {
  894. print_error('nomodules', 'debug');
  895. }
  896. foreach ($mods as $mod => $fullmod) {
  897. if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
  898. continue;
  899. }
  900. // include the module lib once
  901. if (file_exists($fullmod.'/lib.php')) {
  902. // get all instance of the activity
  903. $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
  904. FROM {".$mod."} a, {course_modules} cm, {modules} m
  905. WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
  906. $params = array('mod'=>$mod, 'courseid'=>$courseid);
  907. if ($modinstances = $DB->get_records_sql($sql, $params)) {
  908. foreach ($modinstances as $modinstance) {
  909. grade_update_mod_grades($modinstance);
  910. }
  911. }
  912. }
  913. }
  914. }
  915. /**
  916. * Force full update of module grades in central gradebook
  917. *
  918. * @global object
  919. * @global object
  920. * @param object $modinstance object with extra cmidnumber and modname property
  921. * @param int $userid
  922. * @return boolean success
  923. */
  924. function grade_update_mod_grades($modinstance, $userid=0) {
  925. global $CFG, $DB;
  926. $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
  927. if (!file_exists($fullmod.'/lib.php')) {
  928. debugging('missing lib.php file in module ' . $modinstance->modname);
  929. return false;
  930. }
  931. include_once($fullmod.'/lib.php');
  932. $updategradesfunc = $modinstance->modname.'_update_grades';
  933. $updateitemfunc = $modinstance->modname.'_grade_item_update';
  934. if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
  935. //new grading supported, force updating of grades
  936. $updateitemfunc($modinstance);
  937. $updategradesfunc($modinstance, $userid);
  938. } else {
  939. // mudule does not support grading??
  940. }
  941. return true;
  942. }
  943. /**
  944. * Remove grade letters for given context
  945. *
  946. * @global object
  947. * @param object $context
  948. * @param bool $showfeedback
  949. */
  950. function remove_grade_letters($context, $showfeedback) {
  951. global $DB, $OUTPUT;
  952. $strdeleted = get_string('deleted');
  953. $DB->delete_records('grade_letters', array('contextid'=>$context->id));
  954. if ($showfeedback) {
  955. echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'));
  956. }
  957. }
  958. /**
  959. * Remove all grade related course data - history is kept
  960. *
  961. * @global object
  962. * @param int $courseid
  963. * @param bool $showfeedback print feedback
  964. */
  965. function remove_course_grades($courseid, $showfeedback) {
  966. global $DB, $OUTPUT;
  967. $strdeleted = get_string('deleted');
  968. $course_category = grade_category::fetch_course_category($courseid);
  969. $course_category->delete('coursedelete');
  970. if ($showfeedback) {
  971. echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'));
  972. }
  973. if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
  974. foreach ($outcomes as $outcome) {
  975. $outcome->delete('coursedelete');
  976. }
  977. }
  978. $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
  979. if ($showfeedback) {
  980. echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'));
  981. }
  982. if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
  983. foreach ($scales as $scale) {
  984. $scale->delete('coursedelete');
  985. }
  986. }
  987. if ($showfeedback) {
  988. echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'));
  989. }
  990. $DB->delete_records('grade_settings', array('courseid'=>$courseid));
  991. if ($showfeedback) {
  992. echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'));
  993. }
  994. }
  995. /**
  996. * Called when course category deleted - cleanup gradebook
  997. *
  998. * @global object
  999. * @param int $categoryid course category id
  1000. * @param int $newparentid empty means everything deleted, otherwise id of category where content moved
  1001. * @param bool $showfeedback print feedback
  1002. */
  1003. function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
  1004. global $DB;
  1005. $context = get_context_instance(CONTEXT_COURSECAT, $categoryid);
  1006. $DB->delete_records('grade_letters', array('contextid'=>$context->id));
  1007. }
  1008. /**
  1009. * Does gradebook cleanup when module uninstalled.
  1010. *
  1011. * @global object
  1012. * @global object
  1013. * @param string $modname
  1014. */
  1015. function grade_uninstalled_module($modname) {
  1016. global $CFG, $DB;
  1017. $sql = "SELECT *
  1018. FROM {grade_items}
  1019. WHERE itemtype='mod' AND itemmodule=?";
  1020. // go all items for this module and delete them including the grades
  1021. $rs = $DB->get_recordset_sql($sql, array($modname));
  1022. foreach ($rs as $item) {
  1023. $grade_item = new grade_item($item, false);
  1024. $grade_item->delete('moduninstall');
  1025. }
  1026. $rs->close();
  1027. }
  1028. /**
  1029. * Deletes all user data from gradebook.
  1030. * @param $userid
  1031. */
  1032. function grade_user_delete($userid) {
  1033. if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
  1034. foreach ($grades as $grade) {
  1035. $grade->delete('userdelete');
  1036. }
  1037. }
  1038. }
  1039. /**
  1040. * Purge course data when user unenrolled.
  1041. * @param $userid
  1042. */
  1043. function grade_user_unenrol($courseid, $userid) {
  1044. if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
  1045. foreach ($items as $item) {
  1046. if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
  1047. foreach ($grades as $grade) {
  1048. $grade->delete('userdelete');
  1049. }
  1050. }
  1051. }
  1052. }
  1053. }
  1054. /**
  1055. * Grading cron job
  1056. *
  1057. * @global object
  1058. * @global object
  1059. */
  1060. function grade_cron() {
  1061. global $CFG, $DB;
  1062. $now = time();
  1063. $sql = "SELECT i.*
  1064. FROM {grade_items} i
  1065. WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
  1066. SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
  1067. // go through all courses that have proper final grades and lock them if needed
  1068. $rs = $DB->get_recordset_sql($sql, array($now));
  1069. foreach ($rs as $item) {
  1070. $grade_item = new grade_item($item, false);
  1071. $grade_item->locked = $now;
  1072. $grade_item->update('locktime');
  1073. }
  1074. $rs->close();
  1075. $grade_inst = new grade_grade();
  1076. $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
  1077. $sql = "SELECT $fields
  1078. FROM {grade_grades} g, {grade_items} i
  1079. WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
  1080. SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
  1081. // go through all courses that have proper final grades and lock them if needed
  1082. $rs = $DB->get_recordset_sql($sql, array($now));
  1083. foreach ($rs as $grade) {
  1084. $grade_grade = new grade_grade($grade, false);
  1085. $grade_grade->locked = $now;
  1086. $grade_grade->update('locktime');
  1087. }
  1088. $rs->close();
  1089. //TODO: do not run this cleanup every cron invocation
  1090. // cleanup history tables
  1091. if (!empty($CFG->gradehistorylifetime)) { // value in days
  1092. $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
  1093. $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
  1094. foreach ($tables as $table) {
  1095. if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
  1096. mtrace(" Deleted old grade history records from '$table'");
  1097. }
  1098. }
  1099. }
  1100. }
  1101. /**
  1102. * Resel all course grades
  1103. *
  1104. * @param int $courseid
  1105. * @return bool success
  1106. */
  1107. function grade_course_reset($courseid) {
  1108. // no recalculations
  1109. grade_force_full_regrading($courseid);
  1110. $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
  1111. foreach ($grade_items as $gid=>$grade_item) {
  1112. $grade_item->delete_all_grades('reset');
  1113. }
  1114. //refetch all grades
  1115. grade_grab_course_grades($courseid);
  1116. // recalculate all grades
  1117. grade_regrade_final_grades($courseid);
  1118. return true;
  1119. }
  1120. /**
  1121. * Convert number to 5 decimalfloat, empty string or null db compatible format
  1122. * (we need this to decide if db value changed)
  1123. *
  1124. * @param mixed $number
  1125. * @return mixed float or null
  1126. */
  1127. function grade_floatval($number) {
  1128. if (is_null($number) or $number === '') {
  1129. return null;
  1130. }
  1131. // we must round to 5 digits to get the same precision as in 10,5 db fields
  1132. // note: db rounding for 10,5 is different from php round() function
  1133. return round($number, 5);
  1134. }
  1135. /**
  1136. * Compare two float numbers safely. Uses 5 decimals php precision. Nulls accepted too.
  1137. * Used for skipping of db updates
  1138. *
  1139. * @param float $f1
  1140. * @param float $f2
  1141. * @return bool true if different
  1142. */
  1143. function grade_floats_different($f1, $f2) {
  1144. // note: db rounding for 10,5 is different from php round() function
  1145. return (grade_floatval($f1) !== grade_floatval($f2));
  1146. }
  1147. /**
  1148. * Compare two float numbers safely. Uses 5 decimals php precision.
  1149. *
  1150. * Do not use rounding for 10,5 at the database level as the results may be
  1151. * different from php round() function.
  1152. *
  1153. * @since 2.0
  1154. * @param float $f1
  1155. * @param float $f2
  1156. * @return bool true if the values should be considered as the same grades
  1157. */
  1158. function grade_floats_equal($f1, $f2) {
  1159. return (grade_floatval($f1) === grade_floatval($f2));
  1160. }