PageRenderTime 42ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/grade/grade_item.php

https://bitbucket.org/ceu/moodle_demo
PHP | 1983 lines | 1050 code | 313 blank | 620 comment | 311 complexity | 668fe33c52e78f3c35c2586477d0e653 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.0, LGPL-2.1

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

  1. <?php // $Id: grade_item.php,v 1.130.2.37 2010/04/15 16:37:07 tjhunt Exp $
  2. ///////////////////////////////////////////////////////////////////////////
  3. // //
  4. // NOTICE OF COPYRIGHT //
  5. // //
  6. // Moodle - Modular Object-Oriented Dynamic Learning Environment //
  7. // http://moodle.com //
  8. // //
  9. // Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
  10. // //
  11. // This program is free software; you can redistribute it and/or modify //
  12. // it under the terms of the GNU General Public License as published by //
  13. // the Free Software Foundation; either version 2 of the License, or //
  14. // (at your option) any later version. //
  15. // //
  16. // This program is distributed in the hope that it will be useful, //
  17. // but WITHOUT ANY WARRANTY; without even the implied warranty of //
  18. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
  19. // GNU General Public License for more details: //
  20. // //
  21. // http://www.gnu.org/copyleft/gpl.html //
  22. // //
  23. ///////////////////////////////////////////////////////////////////////////
  24. require_once('grade_object.php');
  25. /**
  26. * Class representing a grade item. It is responsible for handling its DB representation,
  27. * modifying and returning its metadata.
  28. */
  29. class grade_item extends grade_object {
  30. /**
  31. * DB Table (used by grade_object).
  32. * @var string $table
  33. */
  34. var $table = 'grade_items';
  35. /**
  36. * Array of required table fields, must start with 'id'.
  37. * @var array $required_fields
  38. */
  39. var $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
  40. 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
  41. 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
  42. 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
  43. 'timemodified');
  44. /**
  45. * The course this grade_item belongs to.
  46. * @var int $courseid
  47. */
  48. var $courseid;
  49. /**
  50. * The category this grade_item belongs to (optional).
  51. * @var int $categoryid
  52. */
  53. var $categoryid;
  54. /**
  55. * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
  56. * @var object $item_category
  57. */
  58. var $item_category;
  59. /**
  60. * The grade_category object referenced by $this->categoryid.
  61. * @var object $parent_category
  62. */
  63. var $parent_category;
  64. /**
  65. * The name of this grade_item (pushed by the module).
  66. * @var string $itemname
  67. */
  68. var $itemname;
  69. /**
  70. * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
  71. * @var string $itemtype
  72. */
  73. var $itemtype;
  74. /**
  75. * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
  76. * @var string $itemmodule
  77. */
  78. var $itemmodule;
  79. /**
  80. * ID of the item module
  81. * @var int $iteminstance
  82. */
  83. var $iteminstance;
  84. /**
  85. * Number of the item in a series of multiple grades pushed by an activity.
  86. * @var int $itemnumber
  87. */
  88. var $itemnumber;
  89. /**
  90. * Info and notes about this item.
  91. * @var string $iteminfo
  92. */
  93. var $iteminfo;
  94. /**
  95. * Arbitrary idnumber provided by the module responsible.
  96. * @var string $idnumber
  97. */
  98. var $idnumber;
  99. /**
  100. * Calculation string used for this item.
  101. * @var string $calculation
  102. */
  103. var $calculation;
  104. /**
  105. * Indicates if we already tried to normalize the grade calculation formula.
  106. * This flag helps to minimize db access when broken formulas used in calculation.
  107. * @var boolean
  108. */
  109. var $calculation_normalized;
  110. /**
  111. * Math evaluation object
  112. */
  113. var $formula;
  114. /**
  115. * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
  116. * @var int $gradetype
  117. */
  118. var $gradetype = GRADE_TYPE_VALUE;
  119. /**
  120. * Maximum allowable grade.
  121. * @var float $grademax
  122. */
  123. var $grademax = 100;
  124. /**
  125. * Minimum allowable grade.
  126. * @var float $grademin
  127. */
  128. var $grademin = 0;
  129. /**
  130. * id of the scale, if this grade is based on a scale.
  131. * @var int $scaleid
  132. */
  133. var $scaleid;
  134. /**
  135. * A grade_scale object (referenced by $this->scaleid).
  136. * @var object $scale
  137. */
  138. var $scale;
  139. /**
  140. * The id of the optional grade_outcome associated with this grade_item.
  141. * @var int $outcomeid
  142. */
  143. var $outcomeid;
  144. /**
  145. * The grade_outcome this grade is associated with, if applicable.
  146. * @var object $outcome
  147. */
  148. var $outcome;
  149. /**
  150. * grade required to pass. (grademin <= gradepass <= grademax)
  151. * @var float $gradepass
  152. */
  153. var $gradepass = 0;
  154. /**
  155. * Multiply all grades by this number.
  156. * @var float $multfactor
  157. */
  158. var $multfactor = 1.0;
  159. /**
  160. * Add this to all grades.
  161. * @var float $plusfactor
  162. */
  163. var $plusfactor = 0;
  164. /**
  165. * Aggregation coeficient used for weighted averages
  166. * @var float $aggregationcoef
  167. */
  168. var $aggregationcoef = 0;
  169. /**
  170. * Sorting order of the columns.
  171. * @var int $sortorder
  172. */
  173. var $sortorder = 0;
  174. /**
  175. * Display type of the grades (Real, Percentage, Letter, or default).
  176. * @var int $display
  177. */
  178. var $display = GRADE_DISPLAY_TYPE_DEFAULT;
  179. /**
  180. * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
  181. * @var int $decimals
  182. */
  183. var $decimals = null;
  184. /**
  185. * 0 if visible, 1 always hidden or date not visible until
  186. * @var int $hidden
  187. */
  188. var $hidden = 0;
  189. /**
  190. * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
  191. * @var int $locked
  192. */
  193. var $locked = 0;
  194. /**
  195. * Date after which the grade will be locked. Empty means no automatic locking.
  196. * @var int $locktime
  197. */
  198. var $locktime = 0;
  199. /**
  200. * If set, the whole column will be recalculated, then this flag will be switched off.
  201. * @var boolean $needsupdate
  202. */
  203. var $needsupdate = 1;
  204. /**
  205. * Cached dependson array
  206. */
  207. var $dependson_cache = null;
  208. /**
  209. * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
  210. * Force regrading if necessary, rounds the float numbers using php function,
  211. * the reason is we need to compare the db value with computed number to skip regrading if possible.
  212. * @param string $source from where was the object inserted (mod/forum, manual, etc.)
  213. * @return boolean success
  214. */
  215. function update($source=null) {
  216. // reset caches
  217. $this->dependson_cache = null;
  218. // Retrieve scale and infer grademax/min from it if needed
  219. $this->load_scale();
  220. // make sure there is not 0 in outcomeid
  221. if (empty($this->outcomeid)) {
  222. $this->outcomeid = null;
  223. }
  224. if ($this->qualifies_for_regrading()) {
  225. $this->force_regrading();
  226. }
  227. $this->timemodified = time();
  228. $this->grademin = grade_floatval($this->grademin);
  229. $this->grademax = grade_floatval($this->grademax);
  230. $this->multfactor = grade_floatval($this->multfactor);
  231. $this->plusfactor = grade_floatval($this->plusfactor);
  232. $this->aggregationcoef = grade_floatval($this->aggregationcoef);
  233. return parent::update($source);
  234. }
  235. /**
  236. * Compares the values held by this object with those of the matching record in DB, and returns
  237. * whether or not these differences are sufficient to justify an update of all parent objects.
  238. * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
  239. * @return boolean
  240. */
  241. function qualifies_for_regrading() {
  242. if (empty($this->id)) {
  243. return false;
  244. }
  245. $db_item = new grade_item(array('id' => $this->id));
  246. $calculationdiff = $db_item->calculation != $this->calculation;
  247. $categorydiff = $db_item->categoryid != $this->categoryid;
  248. $gradetypediff = $db_item->gradetype != $this->gradetype;
  249. $scaleiddiff = $db_item->scaleid != $this->scaleid;
  250. $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
  251. $locktimediff = $db_item->locktime != $this->locktime;
  252. $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
  253. $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
  254. $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
  255. $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
  256. $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
  257. $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
  258. $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
  259. return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
  260. || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
  261. || $lockeddiff || $acoefdiff || $locktimediff);
  262. }
  263. /**
  264. * Finds and returns a grade_item instance based on params.
  265. * @static
  266. *
  267. * @param array $params associative arrays varname=>value
  268. * @return object grade_item instance or false if none found.
  269. */
  270. function fetch($params) {
  271. return grade_object::fetch_helper('grade_items', 'grade_item', $params);
  272. }
  273. /**
  274. * Finds and returns all grade_item instances based on params.
  275. * @static
  276. *
  277. * @param array $params associative arrays varname=>value
  278. * @return array array of grade_item insatnces or false if none found.
  279. */
  280. function fetch_all($params) {
  281. return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
  282. }
  283. /**
  284. * Delete all grades and force_regrading of parent category.
  285. * @param string $source from where was the object deleted (mod/forum, manual, etc.)
  286. * @return boolean success
  287. */
  288. function delete($source=null) {
  289. $this->delete_all_grades($source);
  290. return parent::delete($source);
  291. }
  292. /**
  293. * Delete all grades
  294. * @param string $source from where was the object deleted (mod/forum, manual, etc.)
  295. * @return boolean success
  296. */
  297. function delete_all_grades($source=null) {
  298. if (!$this->is_course_item()) {
  299. $this->force_regrading();
  300. }
  301. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  302. foreach ($grades as $grade) {
  303. $grade->delete($source);
  304. }
  305. }
  306. return true;
  307. }
  308. /**
  309. * In addition to perform parent::insert(), calls force_regrading() method too.
  310. * @param string $source from where was the object inserted (mod/forum, manual, etc.)
  311. * @return int PK ID if successful, false otherwise
  312. */
  313. function insert($source=null) {
  314. global $CFG;
  315. if (empty($this->courseid)) {
  316. error('Can not insert grade item without course id!');
  317. }
  318. // load scale if needed
  319. $this->load_scale();
  320. // add parent category if needed
  321. if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
  322. $course_category = grade_category::fetch_course_category($this->courseid);
  323. $this->categoryid = $course_category->id;
  324. }
  325. // always place the new items at the end, move them after insert if needed
  326. $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");
  327. if (!empty($last_sortorder)) {
  328. $this->sortorder = $last_sortorder + 1;
  329. } else {
  330. $this->sortorder = 1;
  331. }
  332. // add proper item numbers to manual items
  333. if ($this->itemtype == 'manual') {
  334. if (empty($this->itemnumber)) {
  335. $this->itemnumber = 0;
  336. }
  337. }
  338. // make sure there is not 0 in outcomeid
  339. if (empty($this->outcomeid)) {
  340. $this->outcomeid = null;
  341. }
  342. $this->timecreated = $this->timemodified = time();
  343. if (parent::insert($source)) {
  344. // force regrading of items if needed
  345. $this->force_regrading();
  346. return $this->id;
  347. } else {
  348. debugging("Could not insert this grade_item in the database!");
  349. return false;
  350. }
  351. }
  352. /**
  353. * Set idnumber of grade item, updates also course_modules table
  354. * @param string $idnumber (without magic quotes)
  355. * @return boolean success
  356. */
  357. function add_idnumber($idnumber) {
  358. if (!empty($this->idnumber)) {
  359. return false;
  360. }
  361. if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
  362. if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
  363. return false;
  364. }
  365. if (!empty($cm->idnumber)) {
  366. return false;
  367. }
  368. if (set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id)) {
  369. $this->idnumber = $idnumber;
  370. return $this->update();
  371. }
  372. return false;
  373. } else {
  374. $this->idnumber = $idnumber;
  375. return $this->update();
  376. }
  377. }
  378. /**
  379. * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
  380. * $userid is given) or the locked state of a specific grade within this item if a specific
  381. * $userid is given and the grade_item is unlocked.
  382. *
  383. * @param int $userid
  384. * @return boolean Locked state
  385. */
  386. function is_locked($userid=NULL) {
  387. if (!empty($this->locked)) {
  388. return true;
  389. }
  390. if (!empty($userid)) {
  391. if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
  392. $grade->grade_item =& $this; // prevent db fetching of cached grade_item
  393. return $grade->is_locked();
  394. }
  395. }
  396. return false;
  397. }
  398. /**
  399. * Locks or unlocks this grade_item and (optionally) all its associated final grades.
  400. * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
  401. * @param boolean $cascade lock/unlock child objects too
  402. * @param boolean $refresh refresh grades when unlocking
  403. * @return boolean true if grade_item all grades updated, false if at least one update fails
  404. */
  405. function set_locked($lockedstate, $cascade=false, $refresh=true) {
  406. if ($lockedstate) {
  407. /// setting lock
  408. if ($this->needsupdate) {
  409. return false; // can not lock grade without first having final grade
  410. }
  411. $this->locked = time();
  412. $this->update();
  413. if ($cascade) {
  414. $grades = $this->get_final();
  415. foreach($grades as $g) {
  416. $grade = new grade_grade($g, false);
  417. $grade->grade_item =& $this;
  418. $grade->set_locked(1, null, false);
  419. }
  420. }
  421. return true;
  422. } else {
  423. /// removing lock
  424. if (!empty($this->locked) and $this->locktime < time()) {
  425. //we have to reset locktime or else it would lock up again
  426. $this->locktime = 0;
  427. }
  428. $this->locked = 0;
  429. $this->update();
  430. if ($cascade) {
  431. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  432. foreach($grades as $grade) {
  433. $grade->grade_item =& $this;
  434. $grade->set_locked(0, null, false);
  435. }
  436. }
  437. }
  438. if ($refresh) {
  439. //refresh when unlocking
  440. $this->refresh_grades();
  441. }
  442. return true;
  443. }
  444. }
  445. /**
  446. * Lock the grade if needed - make sure this is called only when final grades are valid
  447. */
  448. function check_locktime() {
  449. if (!empty($this->locked)) {
  450. return; // already locked
  451. }
  452. if ($this->locktime and $this->locktime < time()) {
  453. $this->locked = time();
  454. $this->update('locktime');
  455. }
  456. }
  457. /**
  458. * Set the locktime for this grade item.
  459. *
  460. * @param int $locktime timestamp for lock to activate
  461. * @return void
  462. */
  463. function set_locktime($locktime) {
  464. $this->locktime = $locktime;
  465. $this->update();
  466. }
  467. /**
  468. * Set the locktime for this grade item.
  469. *
  470. * @return int $locktime timestamp for lock to activate
  471. */
  472. function get_locktime() {
  473. return $this->locktime;
  474. }
  475. /**
  476. * Returns the hidden state of this grade_item
  477. * @return boolean hidden state
  478. */
  479. function is_hidden() {
  480. return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
  481. }
  482. /**
  483. * Check grade hidden status. Uses data from both grade item and grade.
  484. * @return boolean true if hiddenuntil, false if not
  485. */
  486. function is_hiddenuntil() {
  487. return $this->hidden > 1;
  488. }
  489. /**
  490. * Check grade item hidden status.
  491. * @return int 0 means visible, 1 hidden always, timestamp hidden until
  492. */
  493. function get_hidden() {
  494. return $this->hidden;
  495. }
  496. /**
  497. * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
  498. * @param int $hidden new hidden status
  499. * @param boolean $cascade apply to child objects too
  500. * @return void
  501. */
  502. function set_hidden($hidden, $cascade=false) {
  503. $this->hidden = $hidden;
  504. $this->update();
  505. if ($cascade) {
  506. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  507. foreach($grades as $grade) {
  508. $grade->grade_item =& $this;
  509. $grade->set_hidden($hidden, $cascade);
  510. }
  511. }
  512. }
  513. //if marking item visible make sure category is visible MDL-21367
  514. if( !$hidden ) {
  515. $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
  516. if ($category_array && array_key_exists($this->categoryid, $category_array)) {
  517. $category = $category_array[$this->categoryid];
  518. //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
  519. //if($category->is_hidden()) {
  520. $category->set_hidden($hidden, false);
  521. //}
  522. }
  523. }
  524. }
  525. /**
  526. * Returns the number of grades that are hidden.
  527. * @param return int Number of hidden grades
  528. */
  529. function has_hidden_grades($groupsql="", $groupwheresql="") {
  530. global $CFG;
  531. return get_field_sql("SELECT COUNT(*) FROM {$CFG->prefix}grade_grades g LEFT JOIN "
  532. ."{$CFG->prefix}user u ON g.userid = u.id $groupsql WHERE itemid = $this->id AND hidden = 1 $groupwheresql");
  533. }
  534. /**
  535. * Mark regrading as finished successfully.
  536. */
  537. function regrading_finished() {
  538. $this->needsupdate = 0;
  539. //do not use $this->update() because we do not want this logged in grade_item_history
  540. set_field('grade_items', 'needsupdate', 0, 'id', $this->id);
  541. }
  542. /**
  543. * Performs the necessary calculations on the grades_final referenced by this grade_item.
  544. * Also resets the needsupdate flag once successfully performed.
  545. *
  546. * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
  547. * because the regrading must be done in correct order!!
  548. *
  549. * @return boolean true if ok, error string otherwise
  550. */
  551. function regrade_final_grades($userid=null) {
  552. global $CFG;
  553. // locked grade items already have correct final grades
  554. if ($this->is_locked()) {
  555. return true;
  556. }
  557. // calculation produces final value using formula from other final values
  558. if ($this->is_calculated()) {
  559. if ($this->compute($userid)) {
  560. return true;
  561. } else {
  562. return "Could not calculate grades for grade item"; // TODO: improve and localize
  563. }
  564. // noncalculated outcomes already have final values - raw grades not used
  565. } else if ($this->is_outcome_item()) {
  566. return true;
  567. // aggregate the category grade
  568. } else if ($this->is_category_item() or $this->is_course_item()) {
  569. // aggregate category grade item
  570. $category = $this->get_item_category();
  571. $category->grade_item =& $this;
  572. if ($category->generate_grades($userid)) {
  573. return true;
  574. } else {
  575. return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
  576. }
  577. } else if ($this->is_manual_item()) {
  578. // manual items track only final grades, no raw grades
  579. return true;
  580. } else if (!$this->is_raw_used()) {
  581. // hmm - raw grades are not used- nothing to regrade
  582. return true;
  583. }
  584. // normal grade item - just new final grades
  585. $result = true;
  586. $grade_inst = new grade_grade();
  587. $fields = implode(',', $grade_inst->required_fields);
  588. if ($userid) {
  589. $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);
  590. } else {
  591. $rs = get_recordset('grade_grades', 'itemid', $this->id, '', $fields);
  592. }
  593. if ($rs) {
  594. while ($grade_record = rs_fetch_next_record($rs)) {
  595. $grade = new grade_grade($grade_record, false);
  596. if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
  597. // this grade is locked - final grade must be ok
  598. continue;
  599. }
  600. $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  601. if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
  602. if (!$grade->update('system')) {
  603. $result = "Internal error updating final grade";
  604. }
  605. }
  606. }
  607. rs_close($rs);
  608. }
  609. return $result;
  610. }
  611. /**
  612. * Given a float grade value or integer grade scale, applies a number of adjustment based on
  613. * grade_item variables and returns the result.
  614. * @param float $rawgrade The raw grade value.
  615. * @param float $rawmin original rawmin
  616. * @param float $rawmax original rawmax
  617. * @return mixed
  618. */
  619. function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
  620. if (is_null($rawgrade)) {
  621. return null;
  622. }
  623. if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
  624. if ($this->grademax < $this->grademin) {
  625. return null;
  626. }
  627. if ($this->grademax == $this->grademin) {
  628. return $this->grademax; // no range
  629. }
  630. // Standardise score to the new grade range
  631. // NOTE: this is not compatible with current assignment grading
  632. if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
  633. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
  634. }
  635. // Apply other grade_item factors
  636. $rawgrade *= $this->multfactor;
  637. $rawgrade += $this->plusfactor;
  638. return $this->bounded_grade($rawgrade);
  639. } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
  640. if (empty($this->scale)) {
  641. $this->load_scale();
  642. }
  643. if ($this->grademax < 0) {
  644. return null; // scale not present - no grade
  645. }
  646. if ($this->grademax == 0) {
  647. return $this->grademax; // only one option
  648. }
  649. // Convert scale if needed
  650. // NOTE: this is not compatible with current assignment grading
  651. if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
  652. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
  653. }
  654. return $this->bounded_grade($rawgrade);
  655. } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
  656. // somebody changed the grading type when grades already existed
  657. return null;
  658. } else {
  659. debugging("Unknown grade type");
  660. return null;
  661. }
  662. }
  663. /**
  664. * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
  665. * @return void
  666. */
  667. function force_regrading() {
  668. $this->needsupdate = 1;
  669. //mark this item and course item only - categories and calculated items are always regraded
  670. $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
  671. set_field_select('grade_items', 'needsupdate', 1, $wheresql);
  672. }
  673. /**
  674. * Instantiates a grade_scale object whose data is retrieved from the DB,
  675. * if this item's scaleid variable is set.
  676. * @return object grade_scale or null if no scale used
  677. */
  678. function load_scale() {
  679. if ($this->gradetype != GRADE_TYPE_SCALE) {
  680. $this->scaleid = null;
  681. }
  682. if (!empty($this->scaleid)) {
  683. //do not load scale if already present
  684. if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
  685. $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
  686. if (!$this->scale) {
  687. debugging('Incorrect scale id: '.$this->scaleid);
  688. $this->scale = null;
  689. return null;
  690. }
  691. $this->scale->load_items();
  692. }
  693. // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
  694. // stay with the current min=1 max=count(scaleitems)
  695. $this->grademax = count($this->scale->scale_items);
  696. $this->grademin = 1;
  697. } else {
  698. $this->scale = null;
  699. }
  700. return $this->scale;
  701. }
  702. /**
  703. * Instantiates a grade_outcome object whose data is retrieved from the DB,
  704. * if this item's outcomeid variable is set.
  705. * @return object grade_outcome
  706. */
  707. function load_outcome() {
  708. if (!empty($this->outcomeid)) {
  709. $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
  710. }
  711. return $this->outcome;
  712. }
  713. /**
  714. * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
  715. * or category attached to category item.
  716. *
  717. * @return mixed grade_category object if applicable, false if course item
  718. */
  719. function get_parent_category() {
  720. if ($this->is_category_item() or $this->is_course_item()) {
  721. return $this->get_item_category();
  722. } else {
  723. return grade_category::fetch(array('id'=>$this->categoryid));
  724. }
  725. }
  726. /**
  727. * Calls upon the get_parent_category method to retrieve the grade_category object
  728. * from the DB and assigns it to $this->parent_category. It also returns the object.
  729. * @return object Grade_category
  730. */
  731. function load_parent_category() {
  732. if (empty($this->parent_category->id)) {
  733. $this->parent_category = $this->get_parent_category();
  734. }
  735. return $this->parent_category;
  736. }
  737. /**
  738. * Returns the grade_category for category item
  739. *
  740. * @return mixed grade_category object if applicable, false otherwise
  741. */
  742. function get_item_category() {
  743. if (!$this->is_course_item() and !$this->is_category_item()) {
  744. return false;
  745. }
  746. return grade_category::fetch(array('id'=>$this->iteminstance));
  747. }
  748. /**
  749. * Calls upon the get_item_category method to retrieve the grade_category object
  750. * from the DB and assigns it to $this->item_category. It also returns the object.
  751. * @return object Grade_category
  752. */
  753. function load_item_category() {
  754. if (empty($this->item_category->id)) {
  755. $this->item_category = $this->get_item_category();
  756. }
  757. return $this->item_category;
  758. }
  759. /**
  760. * Is the grade item associated with category?
  761. * @return boolean
  762. */
  763. function is_category_item() {
  764. return ($this->itemtype == 'category');
  765. }
  766. /**
  767. * Is the grade item associated with course?
  768. * @return boolean
  769. */
  770. function is_course_item() {
  771. return ($this->itemtype == 'course');
  772. }
  773. /**
  774. * Is this a manualy graded item?
  775. * @return boolean
  776. */
  777. function is_manual_item() {
  778. return ($this->itemtype == 'manual');
  779. }
  780. /**
  781. * Is this an outcome item?
  782. * @return boolean
  783. */
  784. function is_outcome_item() {
  785. return !empty($this->outcomeid);
  786. }
  787. /**
  788. * Is the grade item external - associated with module, plugin or something else?
  789. * @return boolean
  790. */
  791. function is_external_item() {
  792. return ($this->itemtype == 'mod');
  793. }
  794. /**
  795. * Is the grade item overridable
  796. * @return boolean
  797. */
  798. function is_overridable_item() {
  799. return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $this->is_course_item() or $this->is_category_item());
  800. }
  801. /**
  802. * Is the grade item feedback overridable
  803. * @return boolean
  804. */
  805. function is_overridable_item_feedback() {
  806. return !$this->is_outcome_item() and $this->is_external_item();
  807. }
  808. /**
  809. * Returns true if grade items uses raw grades
  810. * @return boolean
  811. */
  812. function is_raw_used() {
  813. return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
  814. }
  815. /**
  816. * Returns grade item associated with the course
  817. * @param int $courseid
  818. * @return course item object
  819. */
  820. function fetch_course_item($courseid) {
  821. if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
  822. return $course_item;
  823. }
  824. // first get category - it creates the associated grade item
  825. $course_category = grade_category::fetch_course_category($courseid);
  826. return $course_category->get_grade_item();
  827. }
  828. /**
  829. * Is grading object editable?
  830. * @return boolean
  831. */
  832. function is_editable() {
  833. return true;
  834. }
  835. /**
  836. * Checks if grade calculated. Returns this object's calculation.
  837. * @return boolean true if grade item calculated.
  838. */
  839. function is_calculated() {
  840. if (empty($this->calculation)) {
  841. return false;
  842. }
  843. /*
  844. * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
  845. * we would have to fetch all course grade items to find out the ids.
  846. * Also if user changes the idnumber the formula does not need to be updated.
  847. */
  848. // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
  849. if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
  850. $this->set_calculation($this->calculation);
  851. }
  852. return !empty($this->calculation);
  853. }
  854. /**
  855. * Returns calculation string if grade calculated.
  856. * @return mixed string if calculation used, null if not
  857. */
  858. function get_calculation() {
  859. if ($this->is_calculated()) {
  860. return grade_item::denormalize_formula($this->calculation, $this->courseid);
  861. } else {
  862. return NULL;
  863. }
  864. }
  865. /**
  866. * Sets this item's calculation (creates it) if not yet set, or
  867. * updates it if already set (in the DB). If no calculation is given,
  868. * the calculation is removed.
  869. * @param string $formula string representation of formula used for calculation
  870. * @return boolean success
  871. */
  872. function set_calculation($formula) {
  873. $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
  874. $this->calculation_normalized = true;
  875. return $this->update();
  876. }
  877. /**
  878. * Denormalizes the calculation formula to [idnumber] form
  879. * @static
  880. * @param string $formula
  881. * @return string denormalized string
  882. */
  883. function denormalize_formula($formula, $courseid) {
  884. if (empty($formula)) {
  885. return '';
  886. }
  887. // denormalize formula - convert ##giXX## to [[idnumber]]
  888. if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
  889. foreach ($matches[1] as $id) {
  890. if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
  891. if (!empty($grade_item->idnumber)) {
  892. $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
  893. }
  894. }
  895. }
  896. }
  897. return $formula;
  898. }
  899. /**
  900. * Normalizes the calculation formula to [#giXX#] form
  901. * @static
  902. * @param string $formula
  903. * @return string normalized string
  904. */
  905. function normalize_formula($formula, $courseid) {
  906. $formula = trim($formula);
  907. if (empty($formula)) {
  908. return NULL;
  909. }
  910. // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
  911. if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
  912. foreach ($grade_items as $grade_item) {
  913. $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
  914. }
  915. }
  916. return $formula;
  917. }
  918. /**
  919. * Returns the final values for this grade item (as imported by module or other source).
  920. * @param int $userid Optional: to retrieve a single final grade
  921. * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
  922. */
  923. function get_final($userid=NULL) {
  924. if ($userid) {
  925. if ($user = get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {
  926. return $user;
  927. }
  928. } else {
  929. if ($grades = get_records('grade_grades', 'itemid', $this->id)) {
  930. //TODO: speed up with better SQL
  931. $result = array();
  932. foreach ($grades as $grade) {
  933. $result[$grade->userid] = $grade;
  934. }
  935. return $result;
  936. } else {
  937. return array();
  938. }
  939. }
  940. }
  941. /**
  942. * Get (or create if not exist yet) grade for this user
  943. * @param int $userid
  944. * @return object grade_grade object instance
  945. */
  946. function get_grade($userid, $create=true) {
  947. if (empty($this->id)) {
  948. debugging('Can not use before insert');
  949. return false;
  950. }
  951. $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
  952. if (empty($grade->id) and $create) {
  953. $grade->insert();
  954. }
  955. return $grade;
  956. }
  957. /**
  958. * Returns the sortorder of this grade_item. This method is also available in
  959. * grade_category, for cases where the object type is not know.
  960. * @return int Sort order
  961. */
  962. function get_sortorder() {
  963. return $this->sortorder;
  964. }
  965. /**
  966. * Returns the idnumber of this grade_item. This method is also available in
  967. * grade_category, for cases where the object type is not know.
  968. * @return string idnumber
  969. */
  970. function get_idnumber() {
  971. return $this->idnumber;
  972. }
  973. /**
  974. * Returns this grade_item. This method is also available in
  975. * grade_category, for cases where the object type is not know.
  976. * @return string idnumber
  977. */
  978. function get_grade_item() {
  979. return $this;
  980. }
  981. /**
  982. * Sets the sortorder of this grade_item. This method is also available in
  983. * grade_category, for cases where the object type is not know.
  984. * @param int $sortorder
  985. * @return void
  986. */
  987. function set_sortorder($sortorder) {
  988. if ($this->sortorder == $sortorder) {
  989. return;
  990. }
  991. $this->sortorder = $sortorder;
  992. $this->update();
  993. }
  994. function move_after_sortorder($sortorder) {
  995. global $CFG;
  996. //make some room first
  997. $sql = "UPDATE {$CFG->prefix}grade_items
  998. SET sortorder = sortorder + 1
  999. WHERE sortorder > $sortorder AND courseid = {$this->courseid}";
  1000. execute_sql($sql, false);
  1001. $this->set_sortorder($sortorder + 1);
  1002. }
  1003. /**
  1004. * Returns the most descriptive field for this object. This is a standard method used
  1005. * when we do not know the exact type of an object.
  1006. * @param boolean $fulltotal: if the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
  1007. * @return string name
  1008. */
  1009. function get_name($fulltotal=false) {
  1010. if (!empty($this->itemname)) {
  1011. // MDL-10557
  1012. return format_string($this->itemname);
  1013. } else if ($this->is_course_item()) {
  1014. return get_string('coursetotal', 'grades');
  1015. } else if ($this->is_category_item()) {
  1016. if ($fulltotal) {
  1017. $category = $this->load_parent_category();
  1018. $a = new stdClass();
  1019. $a->category = $category->get_name();
  1020. return get_string('categorytotalfull', 'grades', $a);
  1021. } else {
  1022. return get_string('categorytotal', 'grades');
  1023. }
  1024. } else {
  1025. return get_string('grade');
  1026. }
  1027. }
  1028. /**
  1029. * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
  1030. * @param int $parentid
  1031. * @return boolean success;
  1032. */
  1033. function set_parent($parentid) {
  1034. if ($this->is_course_item() or $this->is_category_item()) {
  1035. error('Can not set parent for category or course item!');
  1036. }
  1037. if ($this->categoryid == $parentid) {
  1038. return true;
  1039. }
  1040. // find parent and check course id
  1041. if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
  1042. return false;
  1043. }
  1044. // MDL-19407 If moving from a non-SWM category to a SWM category, convert aggregationcoef to 0
  1045. $currentparent = $this->load_parent_category();
  1046. if ($currentparent->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN2 && $parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
  1047. $this->aggregationcoef = 0;
  1048. }
  1049. $this->force_regrading();
  1050. // set new parent
  1051. $this->categoryid = $parent_category->id;
  1052. $this->parent_category =& $parent_category;
  1053. return $this->update();
  1054. }
  1055. /**
  1056. * Makes sure value is a valid grade value.
  1057. * @param float $gradevalue
  1058. * @return mixed float or int fixed grade value
  1059. */
  1060. function bounded_grade($gradevalue) {
  1061. global $CFG;
  1062. if (is_null($gradevalue)) {
  1063. return null;
  1064. }
  1065. if ($this->gradetype == GRADE_TYPE_SCALE) {
  1066. // no >100% grades hack for scale grades!
  1067. // 1.5 is rounded to 2 ;-)
  1068. return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
  1069. }
  1070. $grademax = $this->grademax;
  1071. // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
  1072. $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
  1073. if (!empty($CFG->unlimitedgrades)) {
  1074. // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
  1075. $grademax = $grademax * $maxcoef;
  1076. } else if ($this->is_category_item() or $this->is_course_item()) {
  1077. $category = $this->load_item_category();
  1078. if ($category->aggregation >= 100) {
  1079. // grade >100% hack
  1080. $grademax = $grademax * $maxcoef;
  1081. }
  1082. }
  1083. return (float)bounded_number($this->grademin, $gradevalue, $grademax);
  1084. }
  1085. /**
  1086. * Finds out on which other items does this depend directly when doing calculation or category agregation
  1087. * @param bool $reset_cache
  1088. * @return array of grade_item ids this one depends on
  1089. */
  1090. function depends_on($reset_cache=false) {
  1091. global $CFG;
  1092. if ($reset_cache) {
  1093. $this->dependson_cache = null;
  1094. } else if (isset($this->dependson_cache)) {
  1095. return $this->dependson_cache;
  1096. }
  1097. if ($this->is_locked()) {
  1098. // locked items do not need to be regraded
  1099. $this->dependson_cache = array();
  1100. return $this->dependson_cache;
  1101. }
  1102. if ($this->is_calculated()) {
  1103. if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
  1104. $this->dependson_cache = array_unique($matches[1]); // remove duplicates
  1105. return $this->dependson_cache;
  1106. } else {
  1107. $this->dependson_cache = array();
  1108. return $this->dependson_cache;
  1109. }
  1110. } else if ($grade_category = $this->load_item_category()) {
  1111. //only items with numeric or scale values can be aggregated
  1112. if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
  1113. $this->dependson_cache = array();
  1114. return $this->dependson_cache;
  1115. }
  1116. $grade_category->apply_forced_settings();
  1117. if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
  1118. $outcomes_sql = "";
  1119. } else {
  1120. $outcomes_sql = "AND gi.outcomeid IS NULL";
  1121. }
  1122. if (empty($CFG->grade_includescalesinaggregation)) {
  1123. $gtypes = "gi.gradetype = ".GRADE_TYPE_VALUE;
  1124. } else {
  1125. $gtypes = "(gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")";
  1126. }
  1127. if ($grade_category->aggregatesubcats) {
  1128. // return all children excluding category items
  1129. $sql = "SELECT gi.id
  1130. FROM {$CFG->prefix}grade_items gi
  1131. WHERE $gtypes
  1132. $outcomes_sql
  1133. AND gi.categoryid IN (
  1134. SELECT gc.id
  1135. FROM {$CFG->prefix}grade_categories gc
  1136. WHERE gc.path LIKE '%/{$grade_category->id}/%')";
  1137. } else {
  1138. $sql = "SELECT gi.id
  1139. FROM {$CFG->prefix}grade_items gi
  1140. WHERE gi.categoryid = {$grade_category->id}
  1141. AND $gtypes
  1142. $outcomes_sql
  1143. UNION
  1144. SELECT gi.id
  1145. FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc
  1146. WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
  1147. AND gc.parent = {$grade_category->id}
  1148. AND $gtypes
  1149. $outcomes_sql";
  1150. }
  1151. if ($children = get_records_sql($sql)) {
  1152. $this->dependson_cache = array_keys($children);
  1153. return $this->dependson_cache;
  1154. } else {
  1155. $this->dependson_cache = array();
  1156. return $this->dependson_cache;
  1157. }
  1158. } else {
  1159. $this->dependson_cache = array();
  1160. return $this->dependson_cache;
  1161. }
  1162. }
  1163. /**
  1164. * Refetch grades from modules, plugins.
  1165. * @param int $userid optional, one user only
  1166. */
  1167. function refresh_grades($userid=0) {
  1168. if ($this->itemtype == 'mod') {
  1169. if ($this->is_outcome_item()) {
  1170. //nothing to do
  1171. return;
  1172. }
  1173. if (!$activity = get_record($this->itemmodule, 'id', $this->iteminstance)) {
  1174. debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
  1175. return;
  1176. }
  1177. if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
  1178. debugging('Can not find course module');
  1179. return;
  1180. }
  1181. $activity->modname = $this->itemmodule;
  1182. $activity->cmidnumber = $cm->idnumber;
  1183. grade_update_mod_grades($activity);
  1184. }
  1185. }
  1186. /**
  1187. * Updates final grade value for given user, this is a only way to update final
  1188. * grades from gradebook and import because it logs the change in history table
  1189. * and deals with overridden flag. This flag is set to prevent later overriding
  1190. * from raw grades submitted from modules.
  1191. *
  1192. * @param int $userid the graded user
  1193. * @param mixed $finalgrade float value of final grade - false means do not change
  1194. * @param string $howmodified modification source
  1195. * @param string $note optional note
  1196. * @param mixed $feedback teachers feedback as string - false means do not change
  1197. * @param int $feedbackformat
  1198. * @return boolean success
  1199. */
  1200. function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
  1201. global $USER, $CFG;
  1202. $result = true;
  1203. // no grading used or locked
  1204. if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
  1205. return false;
  1206. }
  1207. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
  1208. $grade->grade_item =& $this; // prevent db fetching of this grade_item
  1209. if (empty($usermodified)) {
  1210. $grade->usermodified = $USER->id;
  1211. } else {
  1212. $grade->usermodified = $usermodified;
  1213. }
  1214. if ($grade->is_locked()) {
  1215. // do not update locked grades at all
  1216. return false;
  1217. }
  1218. $locktime = $grade->get_locktime();
  1219. if ($locktime and $locktime < time()) {
  1220. // do not update grades that should be already locked, force regrade instead

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