PageRenderTime 75ms CodeModel.GetById 38ms 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

Large files files are truncated, but you can click here to view the full file

  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($

Large files files are truncated, but you can click here to view the full file