PageRenderTime 34ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/grade/grade_item.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 2142 lines | 1126 code | 320 blank | 696 comment | 326 complexity | 05a529c6c78441bf2bd5a0daf5f651a4 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-3.0
  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. * Definition of a class to represent a grade item
  18. *
  19. * @package core_grades
  20. * @category grade
  21. * @copyright 2006 Nicolas Connault
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once('grade_object.php');
  26. /**
  27. * Class representing a grade item.
  28. *
  29. * It is responsible for handling its DB representation, modifying and returning its metadata.
  30. *
  31. * @package core_grades
  32. * @category grade
  33. * @copyright 2006 Nicolas Connault
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class grade_item extends grade_object {
  37. /**
  38. * DB Table (used by grade_object).
  39. * @var string $table
  40. */
  41. public $table = 'grade_items';
  42. /**
  43. * Array of required table fields, must start with 'id'.
  44. * @var array $required_fields
  45. */
  46. public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
  47. 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
  48. 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
  49. 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
  50. 'timemodified');
  51. /**
  52. * The course this grade_item belongs to.
  53. * @var int $courseid
  54. */
  55. public $courseid;
  56. /**
  57. * The category this grade_item belongs to (optional).
  58. * @var int $categoryid
  59. */
  60. public $categoryid;
  61. /**
  62. * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
  63. * @var grade_category $item_category
  64. */
  65. public $item_category;
  66. /**
  67. * The grade_category object referenced by $this->categoryid.
  68. * @var grade_category $parent_category
  69. */
  70. public $parent_category;
  71. /**
  72. * The name of this grade_item (pushed by the module).
  73. * @var string $itemname
  74. */
  75. public $itemname;
  76. /**
  77. * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
  78. * @var string $itemtype
  79. */
  80. public $itemtype;
  81. /**
  82. * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
  83. * @var string $itemmodule
  84. */
  85. public $itemmodule;
  86. /**
  87. * ID of the item module
  88. * @var int $iteminstance
  89. */
  90. public $iteminstance;
  91. /**
  92. * Number of the item in a series of multiple grades pushed by an activity.
  93. * @var int $itemnumber
  94. */
  95. public $itemnumber;
  96. /**
  97. * Info and notes about this item.
  98. * @var string $iteminfo
  99. */
  100. public $iteminfo;
  101. /**
  102. * Arbitrary idnumber provided by the module responsible.
  103. * @var string $idnumber
  104. */
  105. public $idnumber;
  106. /**
  107. * Calculation string used for this item.
  108. * @var string $calculation
  109. */
  110. public $calculation;
  111. /**
  112. * Indicates if we already tried to normalize the grade calculation formula.
  113. * This flag helps to minimize db access when broken formulas used in calculation.
  114. * @var bool
  115. */
  116. public $calculation_normalized;
  117. /**
  118. * Math evaluation object
  119. * @var calc_formula A formula object
  120. */
  121. public $formula;
  122. /**
  123. * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
  124. * @var int $gradetype
  125. */
  126. public $gradetype = GRADE_TYPE_VALUE;
  127. /**
  128. * Maximum allowable grade.
  129. * @var float $grademax
  130. */
  131. public $grademax = 100;
  132. /**
  133. * Minimum allowable grade.
  134. * @var float $grademin
  135. */
  136. public $grademin = 0;
  137. /**
  138. * id of the scale, if this grade is based on a scale.
  139. * @var int $scaleid
  140. */
  141. public $scaleid;
  142. /**
  143. * The grade_scale object referenced by $this->scaleid.
  144. * @var grade_scale $scale
  145. */
  146. public $scale;
  147. /**
  148. * The id of the optional grade_outcome associated with this grade_item.
  149. * @var int $outcomeid
  150. */
  151. public $outcomeid;
  152. /**
  153. * The grade_outcome this grade is associated with, if applicable.
  154. * @var grade_outcome $outcome
  155. */
  156. public $outcome;
  157. /**
  158. * grade required to pass. (grademin <= gradepass <= grademax)
  159. * @var float $gradepass
  160. */
  161. public $gradepass = 0;
  162. /**
  163. * Multiply all grades by this number.
  164. * @var float $multfactor
  165. */
  166. public $multfactor = 1.0;
  167. /**
  168. * Add this to all grades.
  169. * @var float $plusfactor
  170. */
  171. public $plusfactor = 0;
  172. /**
  173. * Aggregation coeficient used for weighted averages
  174. * @var float $aggregationcoef
  175. */
  176. public $aggregationcoef = 0;
  177. /**
  178. * Sorting order of the columns.
  179. * @var int $sortorder
  180. */
  181. public $sortorder = 0;
  182. /**
  183. * Display type of the grades (Real, Percentage, Letter, or default).
  184. * @var int $display
  185. */
  186. public $display = GRADE_DISPLAY_TYPE_DEFAULT;
  187. /**
  188. * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
  189. * @var int $decimals
  190. */
  191. public $decimals = null;
  192. /**
  193. * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
  194. * @var int $locked
  195. */
  196. public $locked = 0;
  197. /**
  198. * Date after which the grade will be locked. Empty means no automatic locking.
  199. * @var int $locktime
  200. */
  201. public $locktime = 0;
  202. /**
  203. * If set, the whole column will be recalculated, then this flag will be switched off.
  204. * @var bool $needsupdate
  205. */
  206. public $needsupdate = 1;
  207. /**
  208. * Cached dependson array
  209. * @var array An array of cached grade item dependencies.
  210. */
  211. public $dependson_cache = null;
  212. /**
  213. * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
  214. * Force regrading if necessary, rounds the float numbers using php function,
  215. * the reason is we need to compare the db value with computed number to skip regrading if possible.
  216. *
  217. * @param string $source from where was the object inserted (mod/forum, manual, etc.)
  218. * @return bool success
  219. */
  220. public function update($source=null) {
  221. // reset caches
  222. $this->dependson_cache = null;
  223. // Retrieve scale and infer grademax/min from it if needed
  224. $this->load_scale();
  225. // make sure there is not 0 in outcomeid
  226. if (empty($this->outcomeid)) {
  227. $this->outcomeid = null;
  228. }
  229. if ($this->qualifies_for_regrading()) {
  230. $this->force_regrading();
  231. }
  232. $this->timemodified = time();
  233. $this->grademin = grade_floatval($this->grademin);
  234. $this->grademax = grade_floatval($this->grademax);
  235. $this->multfactor = grade_floatval($this->multfactor);
  236. $this->plusfactor = grade_floatval($this->plusfactor);
  237. $this->aggregationcoef = grade_floatval($this->aggregationcoef);
  238. return parent::update($source);
  239. }
  240. /**
  241. * Compares the values held by this object with those of the matching record in DB, and returns
  242. * whether or not these differences are sufficient to justify an update of all parent objects.
  243. * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
  244. *
  245. * @return bool
  246. */
  247. public function qualifies_for_regrading() {
  248. if (empty($this->id)) {
  249. return false;
  250. }
  251. $db_item = new grade_item(array('id' => $this->id));
  252. $calculationdiff = $db_item->calculation != $this->calculation;
  253. $categorydiff = $db_item->categoryid != $this->categoryid;
  254. $gradetypediff = $db_item->gradetype != $this->gradetype;
  255. $scaleiddiff = $db_item->scaleid != $this->scaleid;
  256. $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
  257. $locktimediff = $db_item->locktime != $this->locktime;
  258. $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
  259. $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
  260. $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
  261. $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
  262. $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
  263. $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
  264. $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
  265. return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
  266. || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
  267. || $lockeddiff || $acoefdiff || $locktimediff);
  268. }
  269. /**
  270. * Finds and returns a grade_item instance based on params.
  271. *
  272. * @static
  273. * @param array $params associative arrays varname=>value
  274. * @return grade_item|bool Returns a grade_item instance or false if none found
  275. */
  276. public static function fetch($params) {
  277. return grade_object::fetch_helper('grade_items', 'grade_item', $params);
  278. }
  279. /**
  280. * Finds and returns all grade_item instances based on params.
  281. *
  282. * @static
  283. * @param array $params associative arrays varname=>value
  284. * @return array array of grade_item instances or false if none found.
  285. */
  286. public static function fetch_all($params) {
  287. return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
  288. }
  289. /**
  290. * Delete all grades and force_regrading of parent category.
  291. *
  292. * @param string $source from where was the object deleted (mod/forum, manual, etc.)
  293. * @return bool success
  294. */
  295. public function delete($source=null) {
  296. $this->delete_all_grades($source);
  297. return parent::delete($source);
  298. }
  299. /**
  300. * Delete all grades
  301. *
  302. * @param string $source from where was the object deleted (mod/forum, manual, etc.)
  303. * @return bool
  304. */
  305. public function delete_all_grades($source=null) {
  306. if (!$this->is_course_item()) {
  307. $this->force_regrading();
  308. }
  309. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  310. foreach ($grades as $grade) {
  311. $grade->delete($source);
  312. }
  313. }
  314. return true;
  315. }
  316. /**
  317. * In addition to perform parent::insert(), calls force_regrading() method too.
  318. *
  319. * @param string $source From where was the object inserted (mod/forum, manual, etc.)
  320. * @return int PK ID if successful, false otherwise
  321. */
  322. public function insert($source=null) {
  323. global $CFG, $DB;
  324. if (empty($this->courseid)) {
  325. print_error('cannotinsertgrade');
  326. }
  327. // load scale if needed
  328. $this->load_scale();
  329. // add parent category if needed
  330. if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
  331. $course_category = grade_category::fetch_course_category($this->courseid);
  332. $this->categoryid = $course_category->id;
  333. }
  334. // always place the new items at the end, move them after insert if needed
  335. $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
  336. if (!empty($last_sortorder)) {
  337. $this->sortorder = $last_sortorder + 1;
  338. } else {
  339. $this->sortorder = 1;
  340. }
  341. // add proper item numbers to manual items
  342. if ($this->itemtype == 'manual') {
  343. if (empty($this->itemnumber)) {
  344. $this->itemnumber = 0;
  345. }
  346. }
  347. // make sure there is not 0 in outcomeid
  348. if (empty($this->outcomeid)) {
  349. $this->outcomeid = null;
  350. }
  351. $this->timecreated = $this->timemodified = time();
  352. if (parent::insert($source)) {
  353. // force regrading of items if needed
  354. $this->force_regrading();
  355. return $this->id;
  356. } else {
  357. debugging("Could not insert this grade_item in the database!");
  358. return false;
  359. }
  360. }
  361. /**
  362. * Set idnumber of grade item, updates also course_modules table
  363. *
  364. * @param string $idnumber (without magic quotes)
  365. * @return bool success
  366. */
  367. public function add_idnumber($idnumber) {
  368. global $DB;
  369. if (!empty($this->idnumber)) {
  370. return false;
  371. }
  372. if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
  373. if ($this->itemnumber == 0) {
  374. // for activity modules, itemnumber 0 is synced with the course_modules
  375. if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
  376. return false;
  377. }
  378. if (!empty($cm->idnumber)) {
  379. return false;
  380. }
  381. $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
  382. $this->idnumber = $idnumber;
  383. return $this->update();
  384. } else {
  385. $this->idnumber = $idnumber;
  386. return $this->update();
  387. }
  388. } else {
  389. $this->idnumber = $idnumber;
  390. return $this->update();
  391. }
  392. }
  393. /**
  394. * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
  395. * $userid is given) or the locked state of a specific grade within this item if a specific
  396. * $userid is given and the grade_item is unlocked.
  397. *
  398. * @param int $userid The user's ID
  399. * @return bool Locked state
  400. */
  401. public function is_locked($userid=NULL) {
  402. if (!empty($this->locked)) {
  403. return true;
  404. }
  405. if (!empty($userid)) {
  406. if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
  407. $grade->grade_item =& $this; // prevent db fetching of cached grade_item
  408. return $grade->is_locked();
  409. }
  410. }
  411. return false;
  412. }
  413. /**
  414. * Locks or unlocks this grade_item and (optionally) all its associated final grades.
  415. *
  416. * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
  417. * @param bool $cascade Lock/unlock child objects too
  418. * @param bool $refresh Refresh grades when unlocking
  419. * @return bool True if grade_item all grades updated, false if at least one update fails
  420. */
  421. public function set_locked($lockedstate, $cascade=false, $refresh=true) {
  422. if ($lockedstate) {
  423. /// setting lock
  424. if ($this->needsupdate) {
  425. return false; // can not lock grade without first having final grade
  426. }
  427. $this->locked = time();
  428. $this->update();
  429. if ($cascade) {
  430. $grades = $this->get_final();
  431. foreach($grades as $g) {
  432. $grade = new grade_grade($g, false);
  433. $grade->grade_item =& $this;
  434. $grade->set_locked(1, null, false);
  435. }
  436. }
  437. return true;
  438. } else {
  439. /// removing lock
  440. if (!empty($this->locked) and $this->locktime < time()) {
  441. //we have to reset locktime or else it would lock up again
  442. $this->locktime = 0;
  443. }
  444. $this->locked = 0;
  445. $this->update();
  446. if ($cascade) {
  447. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  448. foreach($grades as $grade) {
  449. $grade->grade_item =& $this;
  450. $grade->set_locked(0, null, false);
  451. }
  452. }
  453. }
  454. if ($refresh) {
  455. //refresh when unlocking
  456. $this->refresh_grades();
  457. }
  458. return true;
  459. }
  460. }
  461. /**
  462. * Lock the grade if needed. Make sure this is called only when final grades are valid
  463. */
  464. public function check_locktime() {
  465. if (!empty($this->locked)) {
  466. return; // already locked
  467. }
  468. if ($this->locktime and $this->locktime < time()) {
  469. $this->locked = time();
  470. $this->update('locktime');
  471. }
  472. }
  473. /**
  474. * Set the locktime for this grade item.
  475. *
  476. * @param int $locktime timestamp for lock to activate
  477. * @return void
  478. */
  479. public function set_locktime($locktime) {
  480. $this->locktime = $locktime;
  481. $this->update();
  482. }
  483. /**
  484. * Set the locktime for this grade item.
  485. *
  486. * @return int $locktime timestamp for lock to activate
  487. */
  488. public function get_locktime() {
  489. return $this->locktime;
  490. }
  491. /**
  492. * Set the hidden status of grade_item and all grades.
  493. *
  494. * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
  495. *
  496. * @param int $hidden new hidden status
  497. * @param bool $cascade apply to child objects too
  498. */
  499. public function set_hidden($hidden, $cascade=false) {
  500. parent::set_hidden($hidden, $cascade);
  501. if ($cascade) {
  502. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  503. foreach($grades as $grade) {
  504. $grade->grade_item =& $this;
  505. $grade->set_hidden($hidden, $cascade);
  506. }
  507. }
  508. }
  509. //if marking item visible make sure category is visible MDL-21367
  510. if( !$hidden ) {
  511. $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
  512. if ($category_array && array_key_exists($this->categoryid, $category_array)) {
  513. $category = $category_array[$this->categoryid];
  514. //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
  515. //if($category->is_hidden()) {
  516. $category->set_hidden($hidden, false);
  517. //}
  518. }
  519. }
  520. }
  521. /**
  522. * Returns the number of grades that are hidden
  523. *
  524. * @param string $groupsql SQL to limit the query by group
  525. * @param array $params SQL params for $groupsql
  526. * @param string $groupwheresql Where conditions for $groupsql
  527. * @return int The number of hidden grades
  528. */
  529. public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
  530. global $DB;
  531. $params = (array)$params;
  532. $params['itemid'] = $this->id;
  533. return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
  534. ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
  535. }
  536. /**
  537. * Mark regrading as finished successfully.
  538. */
  539. public function regrading_finished() {
  540. global $DB;
  541. $this->needsupdate = 0;
  542. //do not use $this->update() because we do not want this logged in grade_item_history
  543. $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
  544. }
  545. /**
  546. * Performs the necessary calculations on the grades_final referenced by this grade_item.
  547. * Also resets the needsupdate flag once successfully performed.
  548. *
  549. * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
  550. * because the regrading must be done in correct order!!
  551. *
  552. * @param int $userid Supply a user ID to limit the regrading to a single user
  553. * @return bool true if ok, error string otherwise
  554. */
  555. public function regrade_final_grades($userid=null) {
  556. global $CFG, $DB;
  557. // locked grade items already have correct final grades
  558. if ($this->is_locked()) {
  559. return true;
  560. }
  561. // calculation produces final value using formula from other final values
  562. if ($this->is_calculated()) {
  563. if ($this->compute($userid)) {
  564. return true;
  565. } else {
  566. return "Could not calculate grades for grade item"; // TODO: improve and localize
  567. }
  568. // noncalculated outcomes already have final values - raw grades not used
  569. } else if ($this->is_outcome_item()) {
  570. return true;
  571. // aggregate the category grade
  572. } else if ($this->is_category_item() or $this->is_course_item()) {
  573. // aggregate category grade item
  574. $category = $this->get_item_category();
  575. $category->grade_item =& $this;
  576. if ($category->generate_grades($userid)) {
  577. return true;
  578. } else {
  579. return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
  580. }
  581. } else if ($this->is_manual_item()) {
  582. // manual items track only final grades, no raw grades
  583. return true;
  584. } else if (!$this->is_raw_used()) {
  585. // hmm - raw grades are not used- nothing to regrade
  586. return true;
  587. }
  588. // normal grade item - just new final grades
  589. $result = true;
  590. $grade_inst = new grade_grade();
  591. $fields = implode(',', $grade_inst->required_fields);
  592. if ($userid) {
  593. $params = array($this->id, $userid);
  594. $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
  595. } else {
  596. $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
  597. }
  598. if ($rs) {
  599. foreach ($rs as $grade_record) {
  600. $grade = new grade_grade($grade_record, false);
  601. if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
  602. // this grade is locked - final grade must be ok
  603. continue;
  604. }
  605. $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  606. if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
  607. if (!$grade->update('system')) {
  608. $result = "Internal error updating final grade";
  609. }
  610. }
  611. }
  612. $rs->close();
  613. }
  614. return $result;
  615. }
  616. /**
  617. * Given a float grade value or integer grade scale, applies a number of adjustment based on
  618. * grade_item variables and returns the result.
  619. *
  620. * @param float $rawgrade The raw grade value
  621. * @param float $rawmin original rawmin
  622. * @param float $rawmax original rawmax
  623. * @return mixed
  624. */
  625. public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
  626. if (is_null($rawgrade)) {
  627. return null;
  628. }
  629. if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
  630. if ($this->grademax < $this->grademin) {
  631. return null;
  632. }
  633. if ($this->grademax == $this->grademin) {
  634. return $this->grademax; // no range
  635. }
  636. // Standardise score to the new grade range
  637. // NOTE: this is not compatible with current assignment grading
  638. $isassignmentmodule = ($this->itemmodule == 'assignment') || ($this->itemmodule == 'assign');
  639. if (!$isassignmentmodule && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
  640. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
  641. }
  642. // Apply other grade_item factors
  643. $rawgrade *= $this->multfactor;
  644. $rawgrade += $this->plusfactor;
  645. return $this->bounded_grade($rawgrade);
  646. } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
  647. if (empty($this->scale)) {
  648. $this->load_scale();
  649. }
  650. if ($this->grademax < 0) {
  651. return null; // scale not present - no grade
  652. }
  653. if ($this->grademax == 0) {
  654. return $this->grademax; // only one option
  655. }
  656. // Convert scale if needed
  657. // NOTE: this is not compatible with current assignment grading
  658. if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
  659. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
  660. }
  661. return $this->bounded_grade($rawgrade);
  662. } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
  663. // somebody changed the grading type when grades already existed
  664. return null;
  665. } else {
  666. debugging("Unknown grade type");
  667. return null;
  668. }
  669. }
  670. /**
  671. * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
  672. *
  673. * @return void
  674. */
  675. public function force_regrading() {
  676. global $DB;
  677. $this->needsupdate = 1;
  678. //mark this item and course item only - categories and calculated items are always regraded
  679. $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
  680. $params = array($this->id, $this->courseid);
  681. $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
  682. }
  683. /**
  684. * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
  685. *
  686. * @return grade_scale Returns a grade_scale object or null if no scale used
  687. */
  688. public function load_scale() {
  689. if ($this->gradetype != GRADE_TYPE_SCALE) {
  690. $this->scaleid = null;
  691. }
  692. if (!empty($this->scaleid)) {
  693. //do not load scale if already present
  694. if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
  695. $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
  696. if (!$this->scale) {
  697. debugging('Incorrect scale id: '.$this->scaleid);
  698. $this->scale = null;
  699. return null;
  700. }
  701. $this->scale->load_items();
  702. }
  703. // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
  704. // stay with the current min=1 max=count(scaleitems)
  705. $this->grademax = count($this->scale->scale_items);
  706. $this->grademin = 1;
  707. } else {
  708. $this->scale = null;
  709. }
  710. return $this->scale;
  711. }
  712. /**
  713. * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
  714. *
  715. * @return grade_outcome This grade item's associated grade_outcome or null
  716. */
  717. public function load_outcome() {
  718. if (!empty($this->outcomeid)) {
  719. $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
  720. }
  721. return $this->outcome;
  722. }
  723. /**
  724. * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
  725. * or category attached to category item.
  726. *
  727. * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
  728. */
  729. public function get_parent_category() {
  730. if ($this->is_category_item() or $this->is_course_item()) {
  731. return $this->get_item_category();
  732. } else {
  733. return grade_category::fetch(array('id'=>$this->categoryid));
  734. }
  735. }
  736. /**
  737. * Calls upon the get_parent_category method to retrieve the grade_category object
  738. * from the DB and assigns it to $this->parent_category. It also returns the object.
  739. *
  740. * @return grade_category This grade item's parent grade_category.
  741. */
  742. public function load_parent_category() {
  743. if (empty($this->parent_category->id)) {
  744. $this->parent_category = $this->get_parent_category();
  745. }
  746. return $this->parent_category;
  747. }
  748. /**
  749. * Returns the grade_category for a grade category grade item
  750. *
  751. * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
  752. */
  753. public function get_item_category() {
  754. if (!$this->is_course_item() and !$this->is_category_item()) {
  755. return false;
  756. }
  757. return grade_category::fetch(array('id'=>$this->iteminstance));
  758. }
  759. /**
  760. * Calls upon the get_item_category method to retrieve the grade_category object
  761. * from the DB and assigns it to $this->item_category. It also returns the object.
  762. *
  763. * @return grade_category
  764. */
  765. public function load_item_category() {
  766. if (empty($this->item_category->id)) {
  767. $this->item_category = $this->get_item_category();
  768. }
  769. return $this->item_category;
  770. }
  771. /**
  772. * Is the grade item associated with category?
  773. *
  774. * @return bool
  775. */
  776. public function is_category_item() {
  777. return ($this->itemtype == 'category');
  778. }
  779. /**
  780. * Is the grade item associated with course?
  781. *
  782. * @return bool
  783. */
  784. public function is_course_item() {
  785. return ($this->itemtype == 'course');
  786. }
  787. /**
  788. * Is this a manually graded item?
  789. *
  790. * @return bool
  791. */
  792. public function is_manual_item() {
  793. return ($this->itemtype == 'manual');
  794. }
  795. /**
  796. * Is this an outcome item?
  797. *
  798. * @return bool
  799. */
  800. public function is_outcome_item() {
  801. return !empty($this->outcomeid);
  802. }
  803. /**
  804. * Is the grade item external - associated with module, plugin or something else?
  805. *
  806. * @return bool
  807. */
  808. public function is_external_item() {
  809. return ($this->itemtype == 'mod');
  810. }
  811. /**
  812. * Is the grade item overridable
  813. *
  814. * @return bool
  815. */
  816. public function is_overridable_item() {
  817. return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $this->is_course_item() or $this->is_category_item());
  818. }
  819. /**
  820. * Is the grade item feedback overridable
  821. *
  822. * @return bool
  823. */
  824. public function is_overridable_item_feedback() {
  825. return !$this->is_outcome_item() and $this->is_external_item();
  826. }
  827. /**
  828. * Returns true if grade items uses raw grades
  829. *
  830. * @return bool
  831. */
  832. public function is_raw_used() {
  833. return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
  834. }
  835. /**
  836. * Returns the grade item associated with the course
  837. *
  838. * @param int $courseid
  839. * @return grade_item Course level grade item object
  840. */
  841. public static function fetch_course_item($courseid) {
  842. if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
  843. return $course_item;
  844. }
  845. // first get category - it creates the associated grade item
  846. $course_category = grade_category::fetch_course_category($courseid);
  847. return $course_category->get_grade_item();
  848. }
  849. /**
  850. * Is grading object editable?
  851. *
  852. * @return bool
  853. */
  854. public function is_editable() {
  855. return true;
  856. }
  857. /**
  858. * Checks if grade calculated. Returns this object's calculation.
  859. *
  860. * @return bool true if grade item calculated.
  861. */
  862. public function is_calculated() {
  863. if (empty($this->calculation)) {
  864. return false;
  865. }
  866. /*
  867. * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
  868. * we would have to fetch all course grade items to find out the ids.
  869. * Also if user changes the idnumber the formula does not need to be updated.
  870. */
  871. // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
  872. if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
  873. $this->set_calculation($this->calculation);
  874. }
  875. return !empty($this->calculation);
  876. }
  877. /**
  878. * Returns calculation string if grade calculated.
  879. *
  880. * @return string Returns the grade item's calculation if calculation is used, null if not
  881. */
  882. public function get_calculation() {
  883. if ($this->is_calculated()) {
  884. return grade_item::denormalize_formula($this->calculation, $this->courseid);
  885. } else {
  886. return NULL;
  887. }
  888. }
  889. /**
  890. * Sets this item's calculation (creates it) if not yet set, or
  891. * updates it if already set (in the DB). If no calculation is given,
  892. * the calculation is removed.
  893. *
  894. * @param string $formula string representation of formula used for calculation
  895. * @return bool success
  896. */
  897. public function set_calculation($formula) {
  898. $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
  899. $this->calculation_normalized = true;
  900. return $this->update();
  901. }
  902. /**
  903. * Denormalizes the calculation formula to [idnumber] form
  904. *
  905. * @param string $formula A string representation of the formula
  906. * @param int $courseid The course ID
  907. * @return string The denormalized formula as a string
  908. */
  909. public static function denormalize_formula($formula, $courseid) {
  910. if (empty($formula)) {
  911. return '';
  912. }
  913. // denormalize formula - convert ##giXX## to [[idnumber]]
  914. if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
  915. foreach ($matches[1] as $id) {
  916. if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
  917. if (!empty($grade_item->idnumber)) {
  918. $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
  919. }
  920. }
  921. }
  922. }
  923. return $formula;
  924. }
  925. /**
  926. * Normalizes the calculation formula to [#giXX#] form
  927. *
  928. * @param string $formula The formula
  929. * @param int $courseid The course ID
  930. * @return string The normalized formula as a string
  931. */
  932. public static function normalize_formula($formula, $courseid) {
  933. $formula = trim($formula);
  934. if (empty($formula)) {
  935. return NULL;
  936. }
  937. // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
  938. if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
  939. foreach ($grade_items as $grade_item) {
  940. $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
  941. }
  942. }
  943. return $formula;
  944. }
  945. /**
  946. * Returns the final values for this grade item (as imported by module or other source).
  947. *
  948. * @param int $userid Optional: to retrieve a single user's final grade
  949. * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
  950. */
  951. public function get_final($userid=NULL) {
  952. global $DB;
  953. if ($userid) {
  954. if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
  955. return $user;
  956. }
  957. } else {
  958. if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
  959. //TODO: speed up with better SQL (MDL-31380)
  960. $result = array();
  961. foreach ($grades as $grade) {
  962. $result[$grade->userid] = $grade;
  963. }
  964. return $result;
  965. } else {
  966. return array();
  967. }
  968. }
  969. }
  970. /**
  971. * Get (or create if not exist yet) grade for this user
  972. *
  973. * @param int $userid The user ID
  974. * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
  975. * @return grade_grade The grade_grade instance for the user for this grade item
  976. */
  977. public function get_grade($userid, $create=true) {
  978. if (empty($this->id)) {
  979. debugging('Can not use before insert');
  980. return false;
  981. }
  982. $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
  983. if (empty($grade->id) and $create) {
  984. $grade->insert();
  985. }
  986. return $grade;
  987. }
  988. /**
  989. * Returns the sortorder of this grade_item. This method is also available in
  990. * grade_category, for cases where the object type is not know.
  991. *
  992. * @return int Sort order
  993. */
  994. public function get_sortorder() {
  995. return $this->sortorder;
  996. }
  997. /**
  998. * Returns the idnumber of this grade_item. This method is also available in
  999. * grade_category, for cases where the object type is not know.
  1000. *
  1001. * @return string The grade item idnumber
  1002. */
  1003. public function get_idnumber() {
  1004. return $this->idnumber;
  1005. }
  1006. /**
  1007. * Returns this grade_item. This method is also available in
  1008. * grade_category, for cases where the object type is not know.
  1009. *
  1010. * @return grade_item
  1011. */
  1012. public function get_grade_item() {
  1013. return $this;
  1014. }
  1015. /**
  1016. * Sets the sortorder of this grade_item. This method is also available in
  1017. * grade_category, for cases where the object type is not know.
  1018. *
  1019. * @param int $sortorder
  1020. */
  1021. public function set_sortorder($sortorder) {
  1022. if ($this->sortorder == $sortorder) {
  1023. return;
  1024. }
  1025. $this->sortorder = $sortorder;
  1026. $this->update();
  1027. }
  1028. /**
  1029. * Update this grade item's sortorder so that it will appear after $sortorder
  1030. *
  1031. * @param int $sortorder The sort order to place this grade item after
  1032. */
  1033. public function move_after_sortorder($sortorder) {
  1034. global $CFG, $DB;
  1035. //make some room first
  1036. $params = array($sortorder, $this->courseid);
  1037. $sql = "UPDATE {grade_items}
  1038. SET sortorder = sortorder + 1
  1039. WHERE sortorder > ? AND courseid = ?";
  1040. $DB->execute($sql, $params);
  1041. $this->set_sortorder($sortorder + 1);
  1042. }
  1043. /**
  1044. * Detect duplicate grade item's sortorder and re-sort them.
  1045. * Note: Duplicate sortorder will be introduced while duplicating activities or
  1046. * merging two courses.
  1047. *
  1048. * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
  1049. */
  1050. public static function fix_duplicate_sortorder($courseid) {
  1051. global $DB;
  1052. $transaction = $DB->start_delegated_transaction();
  1053. $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
  1054. FROM {grade_items} g1
  1055. JOIN {grade_items} g2 ON g1.courseid = g2.courseid
  1056. WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
  1057. ORDER BY g1.sortorder DESC, g1.id DESC";
  1058. // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
  1059. // bottom higher end of the sort orders and work down by id.
  1060. $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
  1061. foreach($rs as $duplicate) {
  1062. $DB->execute("UPDATE {grade_items}
  1063. SET sortorder = sortorder + 1
  1064. WHERE courseid = :courseid AND
  1065. (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
  1066. array('courseid' => $duplicate->courseid,
  1067. 'sortorder' => $duplicate->sortorder,
  1068. 'sortorder2' => $duplicate->sortorder,
  1069. 'id' => $duplicate->id));
  1070. }
  1071. $rs->close();
  1072. $transaction->allow_commit();
  1073. }
  1074. /**
  1075. * Returns the most descriptive field for this object.
  1076. *
  1077. * Determines what type of grade item it is then returns the appropriate string
  1078. *
  1079. * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
  1080. * @return string name
  1081. */
  1082. public function get_name($fulltotal=false) {
  1083. if (!empty($this->itemname)) {
  1084. // MDL-10557
  1085. return format_string($this->itemname);
  1086. } else if ($this->is_course_item()) {
  1087. return get_string('coursetotal', 'grades');
  1088. } else if ($this->is_category_item()) {
  1089. if ($fulltotal) {
  1090. $category = $this->load_parent_category();
  1091. $a = new stdClass();
  1092. $a->category = $category->get_name();
  1093. return get_string('categorytotalfull', 'grades', $a);
  1094. } else {
  1095. return get_string('categorytotal', 'grades');
  1096. }
  1097. } else {
  1098. return get_string('grade');
  1099. }
  1100. }
  1101. /**
  1102. * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
  1103. *
  1104. * @param int $parentid The ID of the new parent
  1105. * @return bool True if success
  1106. */
  1107. public function set_parent($parentid) {
  1108. if ($this->is_course_item() or $this->is_category_item()) {
  1109. print_error('cannotsetparentforcatoritem');
  1110. }
  1111. if ($this->categoryid == $parentid) {
  1112. return true;
  1113. }
  1114. // find parent and check course id
  1115. if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
  1116. return false;
  1117. }
  1118. // MDL-19407 If moving from a non-SWM category to a SWM category, convert aggregationcoef to 0
  1119. $currentparent = $this->load_parent_category();
  1120. if ($currentparent->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN2 && $parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
  1121. $this->aggregationcoef = 0;
  1122. }
  1123. $this->force_regrading();
  1124. // set new parent
  1125. $this->categoryid = $parent_category->id;
  1126. $this->parent_category =& $parent_category;
  1127. return $this->update();
  1128. }
  1129. /**
  1130. * Makes sure value is a valid grade value.
  1131. *
  1132. * @param float $gradevalue
  1133. * @return mixed float or int fixed grade value
  1134. */
  1135. public function bounded_grade($gradevalue) {
  1136. global $CFG;
  1137. if (is_null($gradevalue)) {
  1138. return null;
  1139. }
  1140. if ($this->gradetype == GRADE_TYPE_SCALE) {
  1141. // no >100% grades hack for scale grades!
  1142. // 1.5 is rounded to 2 ;-)
  1143. return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
  1144. }
  1145. $grademax = $this->grademax;
  1146. // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
  1147. $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
  1148. if (!empty($CFG->unlimitedgrades)) {
  1149. // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
  1150. $grademax = $grademax * $maxcoef;
  1151. } else if ($this->is_category_item() or $this->is_course_item()) {
  1152. $category = $this->load_item_category();
  1153. if ($category->aggregation >= 100) {
  1154. // grade >100% hack
  1155. $grademax = $grademax * $maxcoef;
  1156. }
  1157. }
  1158. return (float)bounded_number($this->grademin, $gradevalue, $grademax);
  1159. }
  1160. /**
  1161. * Finds out on which other items does this depend directly when doing calculation or category aggregation
  1162. *
  1163. * @param bool $reset_cache
  1164. * @return array of grade_item IDs this one depends on
  1165. */
  1166. public function depends_on($reset_cache=false) {
  1167. global $CFG, $DB;
  1168. if ($reset_cache) {
  1169. $this->dependson_cache = null;
  1170. } else if (isset($this->dependson_cache)) {
  1171. return $this->dependson_cache;
  1172. }
  1173. if ($this->is_locked()) {
  1174. // locked items do not need to be regraded
  1175. $this->dependson_cache = array();
  1176. return $this->dependson_cache;
  1177. }
  1178. if ($this->is_calculated()) {
  1179. if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
  1180. $this->dependson_cache = array_unique($matches[1]); // remove duplicates
  1181. return $this->dependson_cache;
  1182. } else {
  1183. $this->dependson_cache = array();
  1184. return $this->dependson_cache;
  1185. }
  1186. } else if ($grade_category = $this->load_item_category()) {
  1187. $params = array();
  1188. //only items with numeric or scale values can be aggregated
  1189. if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
  1190. $this->dependson_cache = array();
  1191. return $this->dependson_cache;
  1192. }
  1193. $grade_category->apply_forced_settings();
  1194. if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
  1195. $outcomes_sql = "";
  1196. } else {
  1197. $outcomes_sql = "AND gi.outcomeid IS NULL";
  1198. }
  1199. if (empty($CFG->grade_includescalesinaggregation)) {
  1200. $gtypes = "gi.gradetype = ?";
  1201. $params[] = GRADE_TYPE_VALUE;
  1202. } else {
  1203. $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
  1204. $params[] = GRADE_TYPE_VALUE;
  1205. $params[] = GRADE_TYPE_SCALE;
  1206. }
  1207. if ($grade_category->aggregatesubcats) {
  1208. // return all children excluding category items
  1209. $params[] = $this->courseid;
  1210. $params[] = '%/' . $grade_category->id . '/%';
  1211. $sql = "SELECT gi.id
  1212. FROM {grade_items} gi
  1213. JOIN {grade_categories} gc ON gi.categoryid = gc.id
  1214. WHERE $gtypes
  1215. $outcomes_sql
  1216. AND gi.courseid = ?
  1217. AND gc.path LIKE ?";
  1218. } else {
  1219. $params[] = $grade_category->id;
  1220. $params[] = $this->courseid;
  1221. $params[] = $grade_category->id;
  1222. $params[] = $this->courseid;
  1223. if (empty($CFG->grade_includescalesinaggregation)) {
  1224. $params[] = GRADE_TYPE_VALUE;
  1225. } else {
  1226. $params[] = GRADE_TYPE_VALUE;
  1227. $params[] = GRADE_TYPE_SCALE;
  1228. }
  1229. $sql = "SELECT gi.id
  1230. FROM {grade_items} gi
  1231. WHERE $gtypes
  1232. AND gi.categoryid = ?
  1233. AND gi.courseid = ?
  1234. $outcomes_sql
  1235. UNION
  1236. SELECT gi.id
  1237. FROM {grade_items} gi, {grade_categories} gc
  1238. WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
  1239. AND gc.parent = ?
  1240. AND gi.courseid = ?
  1241. AND $gtypes
  1242. $outcomes_sql";
  1243. }
  1244. if ($children = $DB->get_records_sql($sql, $params)) {
  1245. $this->dependson_cache = array_keys($children);
  1246. return $this->dependson_cache;
  1247. } else {
  1248. $this->dependson_cache = array();
  1249. return $this->dependson_cache;
  1250. }
  1251. } else {
  1252. $this->dependson_cache = array();
  1253. return $this->dependson_cache;
  1254. }
  1255. }
  1256. /**
  1257. * Refetch grades from modules, plugins.
  1258. *
  1259. * @param int $userid optional, limit the refetch to a single user
  1260. * @return bool Returns true on success or if there is nothing to do
  1261. */
  1262. public function refresh_grades($userid=0) {
  1263. global $DB;
  1264. if ($this->itemtype == 'mod') {
  1265. if ($this->is_outcome_item()) {
  1266. //nothing to do
  1267. return true;
  1268. }
  1269. if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
  1270. debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
  1271. return false;
  1272. }
  1273. if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
  1274. debugging('Can not find course module');
  1275. return false;
  1276. }
  1277. $activity->modname = $this->itemmodule;
  1278. $activity->cmidnumber = $cm->idnumber;
  1279. return grade_update_mod_grades($activity, $userid);
  1280. }
  1281. return true;
  1282. }
  1283. /**
  1284. * Updates final grade value for given user, this is a only way to update final
  1285. * grades from gradebook and import because it logs the change in history table
  1286. * and deals with overridden flag. This flag is set to prevent later overriding
  1287. * from raw grades submitted from modules.
  1288. *
  1289. * @param int $userid The graded user
  1290. * @param float|false $finalgrade The float value of final grade, false means do not change
  1291. * @param string $source The modification source
  1292. * @param string $feedback Optional teacher feedback
  1293. * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
  1294. * @param int $usermodified The ID of the user making the modification
  1295. * @return bool success
  1296. */
  1297. public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
  1298. global $USER, $CFG;
  1299. $result = true;
  1300. // no grading used or locked
  1301. if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
  1302. return false;
  1303. }
  1304. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
  1305. $grade->grade_item =& $this; // prevent db fetching of this grade_item
  1306. if (empty($usermodified)) {
  1307. $grade->usermodified = $USER->id;
  1308. } else {
  1309. $grade->usermodified = $usermodified;
  1310. }
  1311. if ($grade->is_locked()) {
  1312. // do not update locked grades at all
  1313. return false;
  1314. }
  1315. $locktime = $grade->get_locktime();
  1316. if ($locktime and $locktime < time()) {
  1317. // do not update grades that should be already locked, force regrade instead
  1318. $this->force_regrading();
  1319. return false;
  1320. }
  1321. $oldgrade = new stdClass();
  1322. $oldgrade->finalgrade = $grade->finalgrade;
  1323. $oldgrade->overridden = $grade->overridden;
  1324. $oldgrade->feedback = $grade->feedback;
  1325. $oldgrade->feedbackformat = $grade->feedbackformat;
  1326. // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
  1327. $grade->rawgrademin = $this->grademin;
  1328. $grade->rawgrademax = $this->grademax;
  1329. $grade->rawscaleid = $this->scaleid;
  1330. // changed grade?
  1331. if ($finalgrade !== false) {
  1332. if ($this->is_overridable_item()) {
  1333. $grade->overridden = time();
  1334. }
  1335. $grade->finalgrade = $this->bounded_grade($finalgrade);
  1336. }
  1337. // do we have comment from teacher?
  1338. if ($feedback !== false) {
  1339. if ($this->is_overridable_item_feedback()) {
  1340. // external items (modules, plugins) may have own feedback
  1341. $grade->overridden = time();
  1342. }
  1343. $grade->feedback = $feedback;
  1344. $grade->feedbackformat = $feedbackformat;
  1345. }
  1346. if (empty($grade->id)) {
  1347. $grade->timecreated = null; // hack alert - date submitted - no submission yet
  1348. $grade->timemodified = time(); // hack alert - date graded
  1349. $result = (bool)$grade->insert($source);
  1350. } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
  1351. or $grade->feedback !== $oldgrade->feedback
  1352. or $grade->feedbackformat != $oldgrade->feedbackformat
  1353. or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
  1354. $grade->timemodified = time(); // hack alert - date graded
  1355. $result = $grade->update($source);
  1356. } else {
  1357. // no grade change
  1358. return $result;
  1359. }
  1360. if (!$result) {
  1361. // something went wrong - better force final grade recalculation
  1362. $this->force_regrading();
  1363. } else if ($this->is_course_item() and !$this->needsupdate) {
  1364. if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
  1365. $this->force_regrading();
  1366. }
  1367. } else if (!$this->needsupdate) {
  1368. $course_item = grade_item::fetch_course_item($this->courseid);
  1369. if (!$course_item->needsupdate) {
  1370. if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
  1371. $this->force_regrading();
  1372. }
  1373. } else {
  1374. $this->force_regrading();
  1375. }
  1376. }
  1377. return $result;
  1378. }
  1379. /**
  1380. * Updates raw grade value for given user, this is a only way to update raw
  1381. * grades from external source (modules, etc.),
  1382. * because it logs the change in history table and deals with final grade recalculation.
  1383. *
  1384. * @param int $userid the graded user
  1385. * @param mixed $rawgrade float value of raw grade - false means do not change
  1386. * @param string $source modification source
  1387. * @param string $feedback optional teacher feedback
  1388. * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
  1389. * @param int $usermodified the ID of the user who did the grading
  1390. * @param int $dategraded A timestamp of when the student's work was graded
  1391. * @param int $datesubmitted A timestamp of when the student's work was submitted
  1392. * @param grade_grade $grade A grade object, useful for bulk upgrades
  1393. * @return bool success
  1394. */
  1395. public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
  1396. global $USER;
  1397. $result = true;
  1398. // calculated grades can not be updated; course and category can not be updated because they are aggregated
  1399. if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
  1400. return false;
  1401. }
  1402. if (is_null($grade)) {
  1403. //fetch from db
  1404. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
  1405. }
  1406. $grade->grade_item =& $this; // prevent db fetching of this grade_item
  1407. if (empty($usermodified)) {
  1408. $grade->usermodified = $USER->id;
  1409. } else {
  1410. $grade->usermodified = $usermodified;
  1411. }
  1412. if ($grade->is_locked()) {
  1413. // do not update locked grades at all
  1414. return false;
  1415. }
  1416. $locktime = $grade->get_locktime();
  1417. if ($locktime and $locktime < time()) {
  1418. // do not update grades that should be already locked and force regrade
  1419. $this->force_regrading();
  1420. return false;
  1421. }
  1422. $oldgrade = new stdClass();
  1423. $oldgrade->finalgrade = $grade->finalgrade;
  1424. $oldgrade->rawgrade = $grade->rawgrade;
  1425. $oldgrade->rawgrademin = $grade->rawgrademin;
  1426. $oldgrade->rawgrademax = $grade->rawgrademax;
  1427. $oldgrade->rawscaleid = $grade->rawscaleid;
  1428. $oldgrade->feedback = $grade->feedback;
  1429. $oldgrade->feedbackformat = $grade->feedbackformat;
  1430. // use new min and max
  1431. $grade->rawgrade = $grade->rawgrade;
  1432. $grade->rawgrademin = $this->grademin;
  1433. $grade->rawgrademax = $this->grademax;
  1434. $grade->rawscaleid = $this->scaleid;
  1435. // change raw grade?
  1436. if ($rawgrade !== false) {
  1437. $grade->rawgrade = $rawgrade;
  1438. }
  1439. // empty feedback means no feedback at all
  1440. if ($feedback === '') {
  1441. $feedback = null;
  1442. }
  1443. // do we have comment from teacher?
  1444. if ($feedback !== false and !$grade->is_overridden()) {
  1445. $grade->feedback = $feedback;
  1446. $grade->feedbackformat = $feedbackformat;
  1447. }
  1448. // update final grade if possible
  1449. if (!$grade->is_locked() and !$grade->is_overridden()) {
  1450. $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  1451. }
  1452. // TODO: hack alert - create new fields for these in 2.0
  1453. $oldgrade->timecreated = $grade->timecreated;
  1454. $oldgrade->timemodified = $grade->timemodified;
  1455. $grade->timecreated = $datesubmitted;
  1456. if ($grade->is_overridden()) {
  1457. // keep original graded date - update_final_grade() sets this for overridden grades
  1458. } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
  1459. // no grade and feedback means no grading yet
  1460. $grade->timemodified = null;
  1461. } else if (!empty($dategraded)) {
  1462. // fine - module sends info when graded (yay!)
  1463. $grade->timemodified = $dategraded;
  1464. } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
  1465. or $grade->feedback !== $oldgrade->feedback) {
  1466. // guess - if either grade or feedback changed set new graded date
  1467. $grade->timemodified = time();
  1468. } else {
  1469. //keep original graded date
  1470. }
  1471. // end of hack alert
  1472. if (empty($grade->id)) {
  1473. $result = (bool)$grade->insert($source);
  1474. } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
  1475. or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)
  1476. or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
  1477. or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
  1478. or $grade->rawscaleid != $oldgrade->rawscaleid
  1479. or $grade->feedback !== $oldgrade->feedback
  1480. or $grade->feedbackformat != $oldgrade->feedbackformat
  1481. or $grade->timecreated != $oldgrade->timecreated // part of hack above
  1482. or $grade->timemodified != $oldgrade->timemodified // part of hack above
  1483. ) {
  1484. $result = $grade->update($source);
  1485. } else {
  1486. return $result;
  1487. }
  1488. if (!$result) {
  1489. // something went wrong - better force final grade recalculation
  1490. $this->force_regrading();
  1491. } else if (!$this->needsupdate) {
  1492. $course_item = grade_item::fetch_course_item($this->courseid);
  1493. if (!$course_item->needsupdate) {
  1494. if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
  1495. $this->force_regrading();
  1496. }
  1497. }
  1498. }
  1499. return $result;
  1500. }
  1501. /**
  1502. * Calculates final grade values using the formula in the calculation property.
  1503. * The parameters are taken from final grades of grade items in current course only.
  1504. *
  1505. * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
  1506. * @return bool false if error
  1507. */
  1508. public function compute($userid=null) {
  1509. global $CFG, $DB;
  1510. if (!$this->is_calculated()) {
  1511. return false;
  1512. }
  1513. require_once($CFG->libdir.'/mathslib.php');
  1514. if ($this->is_locked()) {
  1515. return true; // no need to recalculate locked items
  1516. }
  1517. // Precreate grades - we need them to exist
  1518. if ($userid) {
  1519. $missing = array();
  1520. if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
  1521. $m = new stdClass();
  1522. $m->userid = $userid;
  1523. $missing[] = $m;
  1524. }
  1525. } else {
  1526. // Find any users who have grades for some but not all grade items in this course
  1527. $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
  1528. $sql = "SELECT gg.userid
  1529. FROM {grade_grades} gg
  1530. JOIN {grade_items} gi
  1531. ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
  1532. GROUP BY gg.userid
  1533. HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
  1534. $missing = $DB->get_records_sql($sql, $params);
  1535. }
  1536. if ($missing) {
  1537. foreach ($missing as $m) {
  1538. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
  1539. $grade->grade_item =& $this;
  1540. $grade->insert('system');
  1541. }
  1542. }
  1543. // get used items
  1544. $useditems = $this->depends_on();
  1545. // prepare formula and init maths library
  1546. $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
  1547. if (strpos($formula, '[[') !== false) {
  1548. // missing item
  1549. return false;
  1550. }
  1551. $this->formula = new calc_formula($formula);
  1552. // where to look for final grades?
  1553. // this itemid is added so that we use only one query for source and final grades
  1554. $gis = array_merge($useditems, array($this->id));
  1555. list($usql, $params) = $DB->get_in_or_equal($gis);
  1556. if ($userid) {
  1557. $usersql = "AND g.userid=?";
  1558. $params[] = $userid;
  1559. } else {
  1560. $usersql = "";
  1561. }
  1562. $grade_inst = new grade_grade();
  1563. $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
  1564. $params[] = $this->courseid;
  1565. $sql = "SELECT $fields
  1566. FROM {grade_grades} g, {grade_items} gi
  1567. WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
  1568. ORDER BY g.userid";
  1569. $return = true;
  1570. // group the grades by userid and use formula on the group
  1571. $rs = $DB->get_recordset_sql($sql, $params);
  1572. if ($rs->valid()) {
  1573. $prevuser = 0;
  1574. $grade_records = array();
  1575. $oldgrade = null;
  1576. foreach ($rs as $used) {
  1577. if ($used->userid != $prevuser) {
  1578. if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
  1579. $return = false;
  1580. }
  1581. $prevuser = $used->userid;
  1582. $grade_records = array();
  1583. $oldgrade = null;
  1584. }
  1585. if ($used->itemid == $this->id) {
  1586. $oldgrade = $used;
  1587. }
  1588. $grade_records['gi'.$used->itemid] = $used->finalgrade;
  1589. }
  1590. if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
  1591. $return = false;
  1592. }
  1593. }
  1594. $rs->close();
  1595. return $return;
  1596. }
  1597. /**
  1598. * Internal function that does the final grade calculation
  1599. *
  1600. * @param int $userid The user ID
  1601. * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
  1602. * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
  1603. * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
  1604. * @return bool False if an error occurred
  1605. */
  1606. public function use_formula($userid, $params, $useditems, $oldgrade) {
  1607. if (empty($userid)) {
  1608. return true;
  1609. }
  1610. // add missing final grade values
  1611. // not graded (null) is counted as 0 - the spreadsheet way
  1612. $allinputsnull = true;
  1613. foreach($useditems as $gi) {
  1614. if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
  1615. $params['gi'.$gi] = 0;
  1616. } else {
  1617. $params['gi'.$gi] = (float)$params['gi'.$gi];
  1618. if ($gi != $this->id) {
  1619. $allinputsnull = false;
  1620. }
  1621. }
  1622. }
  1623. // can not use own final grade during calculation
  1624. unset($params['gi'.$this->id]);
  1625. // insert final grade - will be needed later anyway
  1626. if ($oldgrade) {
  1627. $oldfinalgrade = $oldgrade->finalgrade;
  1628. $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
  1629. $grade->grade_item =& $this;
  1630. } else {
  1631. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
  1632. $grade->grade_item =& $this;
  1633. $grade->insert('system');
  1634. $oldfinalgrade = null;
  1635. }
  1636. // no need to recalculate locked or overridden grades
  1637. if ($grade->is_locked() or $grade->is_overridden()) {
  1638. return true;
  1639. }
  1640. if ($allinputsnull) {
  1641. $grade->finalgrade = null;
  1642. $result = true;
  1643. } else {
  1644. // do the calculation
  1645. $this->formula->set_params($params);
  1646. $result = $this->formula->evaluate();
  1647. if ($result === false) {
  1648. $grade->finalgrade = null;
  1649. } else {
  1650. // normalize
  1651. $grade->finalgrade = $this->bounded_grade($result);
  1652. }
  1653. }
  1654. // update in db if changed
  1655. if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
  1656. $grade->timemodified = time();
  1657. $grade->update('compute');
  1658. }
  1659. if ($result !== false) {
  1660. //lock grade if needed
  1661. }
  1662. if ($result === false) {
  1663. return false;
  1664. } else {
  1665. return true;
  1666. }
  1667. }
  1668. /**
  1669. * Validate the formula.
  1670. *
  1671. * @param string $formulastr
  1672. * @return bool true if calculation possible, false otherwise
  1673. */
  1674. public function validate_formula($formulastr) {
  1675. global $CFG, $DB;
  1676. require_once($CFG->libdir.'/mathslib.php');
  1677. $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
  1678. if (empty($formulastr)) {
  1679. return true;
  1680. }
  1681. if (strpos($formulastr, '=') !== 0) {
  1682. return get_string('errorcalculationnoequal', 'grades');
  1683. }
  1684. // get used items
  1685. if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
  1686. $useditems = array_unique($matches[1]); // remove duplicates
  1687. } else {
  1688. $useditems = array();
  1689. }
  1690. // MDL-11902
  1691. // unset the value if formula is trying to reference to itself
  1692. // but array keys does not match itemid
  1693. if (!empty($this->id)) {
  1694. $useditems = array_diff($useditems, array($this->id));
  1695. //unset($useditems[$this->id]);
  1696. }
  1697. // prepare formula and init maths library
  1698. $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
  1699. $formula = new calc_formula($formula);
  1700. if (empty($useditems)) {
  1701. $grade_items = array();
  1702. } else {
  1703. list($usql, $params) = $DB->get_in_or_equal($useditems);
  1704. $params[] = $this->courseid;
  1705. $sql = "SELECT gi.*
  1706. FROM {grade_items} gi
  1707. WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
  1708. if (!$grade_items = $DB->get_records_sql($sql, $params)) {
  1709. $grade_items = array();
  1710. }
  1711. }
  1712. $params = array();
  1713. foreach ($useditems as $itemid) {
  1714. // make sure all grade items exist in this course
  1715. if (!array_key_exists($itemid, $grade_items)) {
  1716. return false;
  1717. }
  1718. // use max grade when testing formula, this should be ok in 99.9%
  1719. // division by 0 is one of possible problems
  1720. $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
  1721. }
  1722. // do the calculation
  1723. $formula->set_params($params);
  1724. $result = $formula->evaluate();
  1725. // false as result indicates some problem
  1726. if ($result === false) {
  1727. // TODO: add more error hints
  1728. return get_string('errorcalculationunknown', 'grades');
  1729. } else {
  1730. return true;
  1731. }
  1732. }
  1733. /**
  1734. * Returns the value of the display type
  1735. *
  1736. * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
  1737. *
  1738. * @return int Display type
  1739. */
  1740. public function get_displaytype() {
  1741. global $CFG;
  1742. if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
  1743. return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
  1744. } else {
  1745. return $this->display;
  1746. }
  1747. }
  1748. /**
  1749. * Returns the value of the decimals field
  1750. *
  1751. * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
  1752. *
  1753. * @return int Decimals (0 - 5)
  1754. */
  1755. public function get_decimals() {
  1756. global $CFG;
  1757. if (is_null($this->decimals)) {
  1758. return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
  1759. } else {
  1760. return $this->decimals;
  1761. }
  1762. }
  1763. /**
  1764. * Returns a string representing the range of grademin - grademax for this grade item.
  1765. *
  1766. * @param int $rangesdisplaytype
  1767. * @param int $rangesdecimalpoints
  1768. * @return string
  1769. */
  1770. function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
  1771. global $USER;
  1772. // Determine which display type to use for this average
  1773. if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
  1774. $displaytype = GRADE_DISPLAY_TYPE_REAL;
  1775. } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
  1776. $displaytype = $this->get_displaytype();
  1777. } else {
  1778. $displaytype = $rangesdisplaytype;
  1779. }
  1780. // Override grade_item setting if a display preference (not default) was set for the averages
  1781. if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
  1782. $decimalpoints = $this->get_decimals();
  1783. } else {
  1784. $decimalpoints = $rangesdecimalpoints;
  1785. }
  1786. if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
  1787. $grademin = "0 %";
  1788. $grademax = "100 %";
  1789. } else {
  1790. $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
  1791. $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
  1792. }
  1793. return $grademin.'&ndash;'. $grademax;
  1794. }
  1795. /**
  1796. * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
  1797. *
  1798. * @return string|false Returns the coefficient string of false is no coefficient is being used
  1799. */
  1800. public function get_coefstring() {
  1801. $parent_category = $this->load_parent_category();
  1802. if ($this->is_category_item()) {
  1803. $parent_category = $parent_category->load_parent_category();
  1804. }
  1805. if ($parent_category->is_aggregationcoef_used()) {
  1806. return $parent_category->get_coefstring();
  1807. } else {
  1808. return false;
  1809. }
  1810. }
  1811. /**
  1812. * Returns whether the grade item can control the visibility of the grades
  1813. *
  1814. * @return bool
  1815. */
  1816. public function can_control_visibility() {
  1817. if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
  1818. return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
  1819. }
  1820. return parent::can_control_visibility();
  1821. }
  1822. }