PageRenderTime 62ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/grade/grade_item.php

http://github.com/moodle/moodle
PHP | 2543 lines | 1339 code | 376 blank | 828 comment | 388 complexity | 67a8292c4521c85fedbcfcf3cd167f8c MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * 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. 'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
  50. 'needsupdate', 'weightoverride', 'timecreated', '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 or extra credit
  174. * @var float $aggregationcoef
  175. */
  176. public $aggregationcoef = 0;
  177. /**
  178. * Aggregation coeficient used for weighted averages only
  179. * @var float $aggregationcoef2
  180. */
  181. public $aggregationcoef2 = 0;
  182. /**
  183. * Sorting order of the columns.
  184. * @var int $sortorder
  185. */
  186. public $sortorder = 0;
  187. /**
  188. * Display type of the grades (Real, Percentage, Letter, or default).
  189. * @var int $display
  190. */
  191. public $display = GRADE_DISPLAY_TYPE_DEFAULT;
  192. /**
  193. * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
  194. * @var int $decimals
  195. */
  196. public $decimals = null;
  197. /**
  198. * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
  199. * @var int $locked
  200. */
  201. public $locked = 0;
  202. /**
  203. * Date after which the grade will be locked. Empty means no automatic locking.
  204. * @var int $locktime
  205. */
  206. public $locktime = 0;
  207. /**
  208. * If set, the whole column will be recalculated, then this flag will be switched off.
  209. * @var bool $needsupdate
  210. */
  211. public $needsupdate = 1;
  212. /**
  213. * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
  214. */
  215. public $weightoverride = 0;
  216. /**
  217. * Cached dependson array
  218. * @var array An array of cached grade item dependencies.
  219. */
  220. public $dependson_cache = null;
  221. /**
  222. * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
  223. *
  224. * @param array $params An array with required parameters for this grade object.
  225. * @param bool $fetch Whether to fetch corresponding row from the database or not,
  226. * optional fields might not be defined if false used
  227. */
  228. public function __construct($params = null, $fetch = true) {
  229. global $CFG;
  230. // Set grademax from $CFG->gradepointdefault .
  231. self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
  232. parent::__construct($params, $fetch);
  233. }
  234. /**
  235. * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
  236. * Force regrading if necessary, rounds the float numbers using php function,
  237. * the reason is we need to compare the db value with computed number to skip regrading if possible.
  238. *
  239. * @param string $source from where was the object inserted (mod/forum, manual, etc.)
  240. * @return bool success
  241. */
  242. public function update($source=null) {
  243. // reset caches
  244. $this->dependson_cache = null;
  245. // Retrieve scale and infer grademax/min from it if needed
  246. $this->load_scale();
  247. // make sure there is not 0 in outcomeid
  248. if (empty($this->outcomeid)) {
  249. $this->outcomeid = null;
  250. }
  251. if ($this->qualifies_for_regrading()) {
  252. $this->force_regrading();
  253. }
  254. $this->timemodified = time();
  255. $this->grademin = grade_floatval($this->grademin);
  256. $this->grademax = grade_floatval($this->grademax);
  257. $this->multfactor = grade_floatval($this->multfactor);
  258. $this->plusfactor = grade_floatval($this->plusfactor);
  259. $this->aggregationcoef = grade_floatval($this->aggregationcoef);
  260. $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
  261. $result = parent::update($source);
  262. if ($result) {
  263. $event = \core\event\grade_item_updated::create_from_grade_item($this);
  264. $event->trigger();
  265. }
  266. return $result;
  267. }
  268. /**
  269. * Compares the values held by this object with those of the matching record in DB, and returns
  270. * whether or not these differences are sufficient to justify an update of all parent objects.
  271. * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
  272. *
  273. * @return bool
  274. */
  275. public function qualifies_for_regrading() {
  276. if (empty($this->id)) {
  277. return false;
  278. }
  279. $db_item = new grade_item(array('id' => $this->id));
  280. $calculationdiff = $db_item->calculation != $this->calculation;
  281. $categorydiff = $db_item->categoryid != $this->categoryid;
  282. $gradetypediff = $db_item->gradetype != $this->gradetype;
  283. $scaleiddiff = $db_item->scaleid != $this->scaleid;
  284. $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
  285. $locktimediff = $db_item->locktime != $this->locktime;
  286. $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
  287. $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
  288. $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
  289. $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
  290. $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
  291. $acoefdiff2 = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
  292. $weightoverride = grade_floats_different($db_item->weightoverride, $this->weightoverride);
  293. $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
  294. $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
  295. return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
  296. || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
  297. || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
  298. }
  299. /**
  300. * Finds and returns a grade_item instance based on params.
  301. *
  302. * @static
  303. * @param array $params associative arrays varname=>value
  304. * @return grade_item|bool Returns a grade_item instance or false if none found
  305. */
  306. public static function fetch($params) {
  307. return grade_object::fetch_helper('grade_items', 'grade_item', $params);
  308. }
  309. /**
  310. * Check to see if there are any existing grades for this grade_item.
  311. *
  312. * @return boolean - true if there are valid grades for this grade_item.
  313. */
  314. public function has_grades() {
  315. global $DB;
  316. $count = $DB->count_records_select('grade_grades',
  317. 'itemid = :gradeitemid AND finalgrade IS NOT NULL',
  318. array('gradeitemid' => $this->id));
  319. return $count > 0;
  320. }
  321. /**
  322. * Check to see if there are existing overridden grades for this grade_item.
  323. *
  324. * @return boolean - true if there are overridden grades for this grade_item.
  325. */
  326. public function has_overridden_grades() {
  327. global $DB;
  328. $count = $DB->count_records_select('grade_grades',
  329. 'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
  330. array('gradeitemid' => $this->id));
  331. return $count > 0;
  332. }
  333. /**
  334. * Finds and returns all grade_item instances based on params.
  335. *
  336. * @static
  337. * @param array $params associative arrays varname=>value
  338. * @return array array of grade_item instances or false if none found.
  339. */
  340. public static function fetch_all($params) {
  341. return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
  342. }
  343. /**
  344. * Delete all grades and force_regrading of parent category.
  345. *
  346. * @param string $source from where was the object deleted (mod/forum, manual, etc.)
  347. * @return bool success
  348. */
  349. public function delete($source=null) {
  350. global $DB;
  351. $transaction = $DB->start_delegated_transaction();
  352. $this->delete_all_grades($source);
  353. $success = parent::delete($source);
  354. $transaction->allow_commit();
  355. return $success;
  356. }
  357. /**
  358. * Delete all grades
  359. *
  360. * @param string $source from where was the object deleted (mod/forum, manual, etc.)
  361. * @return bool
  362. */
  363. public function delete_all_grades($source=null) {
  364. global $DB;
  365. $transaction = $DB->start_delegated_transaction();
  366. if (!$this->is_course_item()) {
  367. $this->force_regrading();
  368. }
  369. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  370. foreach ($grades as $grade) {
  371. $grade->delete($source);
  372. }
  373. }
  374. // Delete all the historical files.
  375. // We only support feedback files for modules atm.
  376. if ($this->is_external_item()) {
  377. $fs = new file_storage();
  378. $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
  379. }
  380. $transaction->allow_commit();
  381. return true;
  382. }
  383. /**
  384. * In addition to perform parent::insert(), calls force_regrading() method too.
  385. *
  386. * @param string $source From where was the object inserted (mod/forum, manual, etc.)
  387. * @return int PK ID if successful, false otherwise
  388. */
  389. public function insert($source=null) {
  390. global $CFG, $DB;
  391. if (empty($this->courseid)) {
  392. print_error('cannotinsertgrade');
  393. }
  394. // load scale if needed
  395. $this->load_scale();
  396. // add parent category if needed
  397. if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
  398. $course_category = grade_category::fetch_course_category($this->courseid);
  399. $this->categoryid = $course_category->id;
  400. }
  401. // always place the new items at the end, move them after insert if needed
  402. $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
  403. if (!empty($last_sortorder)) {
  404. $this->sortorder = $last_sortorder + 1;
  405. } else {
  406. $this->sortorder = 1;
  407. }
  408. // add proper item numbers to manual items
  409. if ($this->itemtype == 'manual') {
  410. if (empty($this->itemnumber)) {
  411. $this->itemnumber = 0;
  412. }
  413. }
  414. // make sure there is not 0 in outcomeid
  415. if (empty($this->outcomeid)) {
  416. $this->outcomeid = null;
  417. }
  418. $this->timecreated = $this->timemodified = time();
  419. if (parent::insert($source)) {
  420. // force regrading of items if needed
  421. $this->force_regrading();
  422. $event = \core\event\grade_item_created::create_from_grade_item($this);
  423. $event->trigger();
  424. return $this->id;
  425. } else {
  426. debugging("Could not insert this grade_item in the database!");
  427. return false;
  428. }
  429. }
  430. /**
  431. * Set idnumber of grade item, updates also course_modules table
  432. *
  433. * @param string $idnumber (without magic quotes)
  434. * @return bool success
  435. */
  436. public function add_idnumber($idnumber) {
  437. global $DB;
  438. if (!empty($this->idnumber)) {
  439. return false;
  440. }
  441. if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
  442. if ($this->itemnumber == 0) {
  443. // for activity modules, itemnumber 0 is synced with the course_modules
  444. if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
  445. return false;
  446. }
  447. if (!empty($cm->idnumber)) {
  448. return false;
  449. }
  450. $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
  451. $this->idnumber = $idnumber;
  452. return $this->update();
  453. } else {
  454. $this->idnumber = $idnumber;
  455. return $this->update();
  456. }
  457. } else {
  458. $this->idnumber = $idnumber;
  459. return $this->update();
  460. }
  461. }
  462. /**
  463. * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
  464. * $userid is given) or the locked state of a specific grade within this item if a specific
  465. * $userid is given and the grade_item is unlocked.
  466. *
  467. * @param int $userid The user's ID
  468. * @return bool Locked state
  469. */
  470. public function is_locked($userid=NULL) {
  471. global $CFG;
  472. // Override for any grade items belonging to activities which are in the process of being deleted.
  473. require_once($CFG->dirroot . '/course/lib.php');
  474. if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
  475. return true;
  476. }
  477. if (!empty($this->locked)) {
  478. return true;
  479. }
  480. if (!empty($userid)) {
  481. if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
  482. $grade->grade_item =& $this; // prevent db fetching of cached grade_item
  483. return $grade->is_locked();
  484. }
  485. }
  486. return false;
  487. }
  488. /**
  489. * Locks or unlocks this grade_item and (optionally) all its associated final grades.
  490. *
  491. * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
  492. * @param bool $cascade Lock/unlock child objects too
  493. * @param bool $refresh Refresh grades when unlocking
  494. * @return bool True if grade_item all grades updated, false if at least one update fails
  495. */
  496. public function set_locked($lockedstate, $cascade=false, $refresh=true) {
  497. if ($lockedstate) {
  498. /// setting lock
  499. if ($this->needsupdate) {
  500. return false; // can not lock grade without first having final grade
  501. }
  502. $this->locked = time();
  503. $this->update();
  504. if ($cascade) {
  505. $grades = $this->get_final();
  506. foreach($grades as $g) {
  507. $grade = new grade_grade($g, false);
  508. $grade->grade_item =& $this;
  509. $grade->set_locked(1, null, false);
  510. }
  511. }
  512. return true;
  513. } else {
  514. /// removing lock
  515. if (!empty($this->locked) and $this->locktime < time()) {
  516. //we have to reset locktime or else it would lock up again
  517. $this->locktime = 0;
  518. }
  519. $this->locked = 0;
  520. $this->update();
  521. if ($cascade) {
  522. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  523. foreach($grades as $grade) {
  524. $grade->grade_item =& $this;
  525. $grade->set_locked(0, null, false);
  526. }
  527. }
  528. }
  529. if ($refresh) {
  530. //refresh when unlocking
  531. $this->refresh_grades();
  532. }
  533. return true;
  534. }
  535. }
  536. /**
  537. * Lock the grade if needed. Make sure this is called only when final grades are valid
  538. */
  539. public function check_locktime() {
  540. if (!empty($this->locked)) {
  541. return; // already locked
  542. }
  543. if ($this->locktime and $this->locktime < time()) {
  544. $this->locked = time();
  545. $this->update('locktime');
  546. }
  547. }
  548. /**
  549. * Set the locktime for this grade item.
  550. *
  551. * @param int $locktime timestamp for lock to activate
  552. * @return void
  553. */
  554. public function set_locktime($locktime) {
  555. $this->locktime = $locktime;
  556. $this->update();
  557. }
  558. /**
  559. * Set the locktime for this grade item.
  560. *
  561. * @return int $locktime timestamp for lock to activate
  562. */
  563. public function get_locktime() {
  564. return $this->locktime;
  565. }
  566. /**
  567. * Set the hidden status of grade_item and all grades.
  568. *
  569. * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
  570. *
  571. * @param int $hidden new hidden status
  572. * @param bool $cascade apply to child objects too
  573. */
  574. public function set_hidden($hidden, $cascade=false) {
  575. parent::set_hidden($hidden, $cascade);
  576. if ($cascade) {
  577. if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
  578. foreach($grades as $grade) {
  579. $grade->grade_item =& $this;
  580. $grade->set_hidden($hidden, $cascade);
  581. }
  582. }
  583. }
  584. //if marking item visible make sure category is visible MDL-21367
  585. if( !$hidden ) {
  586. $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
  587. if ($category_array && array_key_exists($this->categoryid, $category_array)) {
  588. $category = $category_array[$this->categoryid];
  589. //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
  590. $category->set_hidden($hidden, false);
  591. }
  592. }
  593. }
  594. /**
  595. * Returns the number of grades that are hidden
  596. *
  597. * @param string $groupsql SQL to limit the query by group
  598. * @param array $params SQL params for $groupsql
  599. * @param string $groupwheresql Where conditions for $groupsql
  600. * @return int The number of hidden grades
  601. */
  602. public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
  603. global $DB;
  604. $params = (array)$params;
  605. $params['itemid'] = $this->id;
  606. return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
  607. ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
  608. }
  609. /**
  610. * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
  611. * Situations such as an error being found will still result in the regrading being finished.
  612. */
  613. public function regrading_finished() {
  614. global $DB;
  615. $this->needsupdate = 0;
  616. //do not use $this->update() because we do not want this logged in grade_item_history
  617. $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
  618. }
  619. /**
  620. * Performs the necessary calculations on the grades_final referenced by this grade_item.
  621. * Also resets the needsupdate flag once successfully performed.
  622. *
  623. * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
  624. * because the regrading must be done in correct order!!
  625. *
  626. * @param int $userid Supply a user ID to limit the regrading to a single user
  627. * @return bool true if ok, error string otherwise
  628. */
  629. public function regrade_final_grades($userid=null) {
  630. global $CFG, $DB;
  631. // locked grade items already have correct final grades
  632. if ($this->is_locked()) {
  633. return true;
  634. }
  635. // calculation produces final value using formula from other final values
  636. if ($this->is_calculated()) {
  637. if ($this->compute($userid)) {
  638. return true;
  639. } else {
  640. return "Could not calculate grades for grade item"; // TODO: improve and localize
  641. }
  642. // noncalculated outcomes already have final values - raw grades not used
  643. } else if ($this->is_outcome_item()) {
  644. return true;
  645. // aggregate the category grade
  646. } else if ($this->is_category_item() or $this->is_course_item()) {
  647. // aggregate category grade item
  648. $category = $this->load_item_category();
  649. $category->grade_item =& $this;
  650. if ($category->generate_grades($userid)) {
  651. return true;
  652. } else {
  653. return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
  654. }
  655. } else if ($this->is_manual_item()) {
  656. // manual items track only final grades, no raw grades
  657. return true;
  658. } else if (!$this->is_raw_used()) {
  659. // hmm - raw grades are not used- nothing to regrade
  660. return true;
  661. }
  662. // normal grade item - just new final grades
  663. $result = true;
  664. $grade_inst = new grade_grade();
  665. $fields = implode(',', $grade_inst->required_fields);
  666. if ($userid) {
  667. $params = array($this->id, $userid);
  668. $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
  669. } else {
  670. $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
  671. }
  672. if ($rs) {
  673. foreach ($rs as $grade_record) {
  674. $grade = new grade_grade($grade_record, false);
  675. if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
  676. // this grade is locked - final grade must be ok
  677. continue;
  678. }
  679. $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  680. if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
  681. $success = $grade->update('system');
  682. // If successful trigger a user_graded event.
  683. if ($success) {
  684. $grade->load_grade_item();
  685. \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
  686. } else {
  687. $result = "Internal error updating final grade";
  688. }
  689. }
  690. }
  691. $rs->close();
  692. }
  693. return $result;
  694. }
  695. /**
  696. * Given a float grade value or integer grade scale, applies a number of adjustment based on
  697. * grade_item variables and returns the result.
  698. *
  699. * @param float $rawgrade The raw grade value
  700. * @param float $rawmin original rawmin
  701. * @param float $rawmax original rawmax
  702. * @return mixed
  703. */
  704. public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
  705. if (is_null($rawgrade)) {
  706. return null;
  707. }
  708. if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
  709. if ($this->grademax < $this->grademin) {
  710. return null;
  711. }
  712. if ($this->grademax == $this->grademin) {
  713. return $this->grademax; // no range
  714. }
  715. // Standardise score to the new grade range
  716. // NOTE: skip if the activity provides a manual rescaling option.
  717. $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
  718. if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
  719. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
  720. }
  721. // Apply other grade_item factors
  722. $rawgrade *= $this->multfactor;
  723. $rawgrade += $this->plusfactor;
  724. return $this->bounded_grade($rawgrade);
  725. } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
  726. if (empty($this->scale)) {
  727. $this->load_scale();
  728. }
  729. if ($this->grademax < 0) {
  730. return null; // scale not present - no grade
  731. }
  732. if ($this->grademax == 0) {
  733. return $this->grademax; // only one option
  734. }
  735. // Convert scale if needed
  736. // NOTE: skip if the activity provides a manual rescaling option.
  737. $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
  738. if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
  739. // This should never happen because scales are locked if they are in use.
  740. $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
  741. }
  742. return $this->bounded_grade($rawgrade);
  743. } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
  744. // somebody changed the grading type when grades already existed
  745. return null;
  746. } else {
  747. debugging("Unknown grade type");
  748. return null;
  749. }
  750. }
  751. /**
  752. * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
  753. * Scale every rawgrade to maintain the percentage. This function should be called
  754. * after the gradeitem has been updated to the new min and max values.
  755. *
  756. * @param float $oldgrademin The previous grade min value
  757. * @param float $oldgrademax The previous grade max value
  758. * @param float $newgrademin The new grade min value
  759. * @param float $newgrademax The new grade max value
  760. * @param string $source from where was the object inserted (mod/forum, manual, etc.)
  761. * @return bool True on success
  762. */
  763. public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
  764. global $DB;
  765. if (empty($this->id)) {
  766. return false;
  767. }
  768. if ($oldgrademax <= $oldgrademin) {
  769. // Grades cannot be scaled.
  770. return false;
  771. }
  772. $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
  773. if (($newgrademax - $newgrademin) <= 1) {
  774. // We would lose too much precision, lets bail.
  775. return false;
  776. }
  777. $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
  778. foreach ($rs as $graderecord) {
  779. // For each record, create an object to work on.
  780. $grade = new grade_grade($graderecord, false);
  781. // Set this object in the item so it doesn't re-fetch it.
  782. $grade->grade_item = $this;
  783. if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
  784. // Updating the raw grade automatically updates the min/max.
  785. if ($this->is_raw_used()) {
  786. $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
  787. $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
  788. } else {
  789. $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
  790. $this->update_final_grade($grade->userid, $finalgrade, $source);
  791. }
  792. }
  793. }
  794. $rs->close();
  795. // Mark this item for regrading.
  796. $this->force_regrading();
  797. return true;
  798. }
  799. /**
  800. * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
  801. *
  802. * @return void
  803. */
  804. public function force_regrading() {
  805. global $DB;
  806. $this->needsupdate = 1;
  807. //mark this item and course item only - categories and calculated items are always regraded
  808. $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
  809. $params = array($this->id, $this->courseid);
  810. $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
  811. }
  812. /**
  813. * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
  814. *
  815. * @return grade_scale Returns a grade_scale object or null if no scale used
  816. */
  817. public function load_scale() {
  818. if ($this->gradetype != GRADE_TYPE_SCALE) {
  819. $this->scaleid = null;
  820. }
  821. if (!empty($this->scaleid)) {
  822. //do not load scale if already present
  823. if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
  824. $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
  825. if (!$this->scale) {
  826. debugging('Incorrect scale id: '.$this->scaleid);
  827. $this->scale = null;
  828. return null;
  829. }
  830. $this->scale->load_items();
  831. }
  832. // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
  833. // stay with the current min=1 max=count(scaleitems)
  834. $this->grademax = count($this->scale->scale_items);
  835. $this->grademin = 1;
  836. } else {
  837. $this->scale = null;
  838. }
  839. return $this->scale;
  840. }
  841. /**
  842. * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
  843. *
  844. * @return grade_outcome This grade item's associated grade_outcome or null
  845. */
  846. public function load_outcome() {
  847. if (!empty($this->outcomeid)) {
  848. $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
  849. }
  850. return $this->outcome;
  851. }
  852. /**
  853. * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
  854. * or category attached to category item.
  855. *
  856. * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
  857. */
  858. public function get_parent_category() {
  859. if ($this->is_category_item() or $this->is_course_item()) {
  860. return $this->get_item_category();
  861. } else {
  862. return grade_category::fetch(array('id'=>$this->categoryid));
  863. }
  864. }
  865. /**
  866. * Calls upon the get_parent_category method to retrieve the grade_category object
  867. * from the DB and assigns it to $this->parent_category. It also returns the object.
  868. *
  869. * @return grade_category This grade item's parent grade_category.
  870. */
  871. public function load_parent_category() {
  872. if (empty($this->parent_category->id)) {
  873. $this->parent_category = $this->get_parent_category();
  874. }
  875. return $this->parent_category;
  876. }
  877. /**
  878. * Returns the grade_category for a grade category grade item
  879. *
  880. * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
  881. */
  882. public function get_item_category() {
  883. if (!$this->is_course_item() and !$this->is_category_item()) {
  884. return false;
  885. }
  886. return grade_category::fetch(array('id'=>$this->iteminstance));
  887. }
  888. /**
  889. * Calls upon the get_item_category method to retrieve the grade_category object
  890. * from the DB and assigns it to $this->item_category. It also returns the object.
  891. *
  892. * @return grade_category
  893. */
  894. public function load_item_category() {
  895. if (empty($this->item_category->id)) {
  896. $this->item_category = $this->get_item_category();
  897. }
  898. return $this->item_category;
  899. }
  900. /**
  901. * Is the grade item associated with category?
  902. *
  903. * @return bool
  904. */
  905. public function is_category_item() {
  906. return ($this->itemtype == 'category');
  907. }
  908. /**
  909. * Is the grade item associated with course?
  910. *
  911. * @return bool
  912. */
  913. public function is_course_item() {
  914. return ($this->itemtype == 'course');
  915. }
  916. /**
  917. * Is this a manually graded item?
  918. *
  919. * @return bool
  920. */
  921. public function is_manual_item() {
  922. return ($this->itemtype == 'manual');
  923. }
  924. /**
  925. * Is this an outcome item?
  926. *
  927. * @return bool
  928. */
  929. public function is_outcome_item() {
  930. return !empty($this->outcomeid);
  931. }
  932. /**
  933. * Is the grade item external - associated with module, plugin or something else?
  934. *
  935. * @return bool
  936. */
  937. public function is_external_item() {
  938. return ($this->itemtype == 'mod');
  939. }
  940. /**
  941. * Is the grade item overridable
  942. *
  943. * @return bool
  944. */
  945. public function is_overridable_item() {
  946. if ($this->is_course_item() or $this->is_category_item()) {
  947. $overridable = (bool) get_config('moodle', 'grade_overridecat');
  948. } else {
  949. $overridable = false;
  950. }
  951. return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
  952. }
  953. /**
  954. * Is the grade item feedback overridable
  955. *
  956. * @return bool
  957. */
  958. public function is_overridable_item_feedback() {
  959. return !$this->is_outcome_item() and $this->is_external_item();
  960. }
  961. /**
  962. * Returns true if grade items uses raw grades
  963. *
  964. * @return bool
  965. */
  966. public function is_raw_used() {
  967. return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
  968. }
  969. /**
  970. * Returns true if the grade item is an aggreggated type grade.
  971. *
  972. * @since Moodle 2.8.7, 2.9.1
  973. * @return bool
  974. */
  975. public function is_aggregate_item() {
  976. return ($this->is_category_item() || $this->is_course_item());
  977. }
  978. /**
  979. * Returns the grade item associated with the course
  980. *
  981. * @param int $courseid
  982. * @return grade_item Course level grade item object
  983. */
  984. public static function fetch_course_item($courseid) {
  985. if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
  986. return $course_item;
  987. }
  988. // first get category - it creates the associated grade item
  989. $course_category = grade_category::fetch_course_category($courseid);
  990. return $course_category->get_grade_item();
  991. }
  992. /**
  993. * Is grading object editable?
  994. *
  995. * @return bool
  996. */
  997. public function is_editable() {
  998. return true;
  999. }
  1000. /**
  1001. * Checks if grade calculated. Returns this object's calculation.
  1002. *
  1003. * @return bool true if grade item calculated.
  1004. */
  1005. public function is_calculated() {
  1006. if (empty($this->calculation)) {
  1007. return false;
  1008. }
  1009. /*
  1010. * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
  1011. * we would have to fetch all course grade items to find out the ids.
  1012. * Also if user changes the idnumber the formula does not need to be updated.
  1013. */
  1014. // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
  1015. if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
  1016. $this->set_calculation($this->calculation);
  1017. }
  1018. return !empty($this->calculation);
  1019. }
  1020. /**
  1021. * Returns calculation string if grade calculated.
  1022. *
  1023. * @return string Returns the grade item's calculation if calculation is used, null if not
  1024. */
  1025. public function get_calculation() {
  1026. if ($this->is_calculated()) {
  1027. return grade_item::denormalize_formula($this->calculation, $this->courseid);
  1028. } else {
  1029. return NULL;
  1030. }
  1031. }
  1032. /**
  1033. * Sets this item's calculation (creates it) if not yet set, or
  1034. * updates it if already set (in the DB). If no calculation is given,
  1035. * the calculation is removed.
  1036. *
  1037. * @param string $formula string representation of formula used for calculation
  1038. * @return bool success
  1039. */
  1040. public function set_calculation($formula) {
  1041. $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
  1042. $this->calculation_normalized = true;
  1043. return $this->update();
  1044. }
  1045. /**
  1046. * Denormalizes the calculation formula to [idnumber] form
  1047. *
  1048. * @param string $formula A string representation of the formula
  1049. * @param int $courseid The course ID
  1050. * @return string The denormalized formula as a string
  1051. */
  1052. public static function denormalize_formula($formula, $courseid) {
  1053. if (empty($formula)) {
  1054. return '';
  1055. }
  1056. // denormalize formula - convert ##giXX## to [[idnumber]]
  1057. if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
  1058. foreach ($matches[1] as $id) {
  1059. if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
  1060. if (!empty($grade_item->idnumber)) {
  1061. $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
  1062. }
  1063. }
  1064. }
  1065. }
  1066. return $formula;
  1067. }
  1068. /**
  1069. * Normalizes the calculation formula to [#giXX#] form
  1070. *
  1071. * @param string $formula The formula
  1072. * @param int $courseid The course ID
  1073. * @return string The normalized formula as a string
  1074. */
  1075. public static function normalize_formula($formula, $courseid) {
  1076. $formula = trim($formula);
  1077. if (empty($formula)) {
  1078. return NULL;
  1079. }
  1080. // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
  1081. if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
  1082. foreach ($grade_items as $grade_item) {
  1083. $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
  1084. }
  1085. }
  1086. return $formula;
  1087. }
  1088. /**
  1089. * Returns the final values for this grade item (as imported by module or other source).
  1090. *
  1091. * @param int $userid Optional: to retrieve a single user's final grade
  1092. * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
  1093. */
  1094. public function get_final($userid=NULL) {
  1095. global $DB;
  1096. if ($userid) {
  1097. if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
  1098. return $user;
  1099. }
  1100. } else {
  1101. if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
  1102. //TODO: speed up with better SQL (MDL-31380)
  1103. $result = array();
  1104. foreach ($grades as $grade) {
  1105. $result[$grade->userid] = $grade;
  1106. }
  1107. return $result;
  1108. } else {
  1109. return array();
  1110. }
  1111. }
  1112. }
  1113. /**
  1114. * Get (or create if not exist yet) grade for this user
  1115. *
  1116. * @param int $userid The user ID
  1117. * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
  1118. * @return grade_grade The grade_grade instance for the user for this grade item
  1119. */
  1120. public function get_grade($userid, $create=true) {
  1121. if (empty($this->id)) {
  1122. debugging('Can not use before insert');
  1123. return false;
  1124. }
  1125. $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
  1126. if (empty($grade->id) and $create) {
  1127. $grade->insert();
  1128. }
  1129. return $grade;
  1130. }
  1131. /**
  1132. * Returns the sortorder of this grade_item. This method is also available in
  1133. * grade_category, for cases where the object type is not know.
  1134. *
  1135. * @return int Sort order
  1136. */
  1137. public function get_sortorder() {
  1138. return $this->sortorder;
  1139. }
  1140. /**
  1141. * Returns the idnumber of this grade_item. This method is also available in
  1142. * grade_category, for cases where the object type is not know.
  1143. *
  1144. * @return string The grade item idnumber
  1145. */
  1146. public function get_idnumber() {
  1147. return $this->idnumber;
  1148. }
  1149. /**
  1150. * Returns this grade_item. This method is also available in
  1151. * grade_category, for cases where the object type is not know.
  1152. *
  1153. * @return grade_item
  1154. */
  1155. public function get_grade_item() {
  1156. return $this;
  1157. }
  1158. /**
  1159. * Sets the sortorder of this grade_item. This method is also available in
  1160. * grade_category, for cases where the object type is not know.
  1161. *
  1162. * @param int $sortorder
  1163. */
  1164. public function set_sortorder($sortorder) {
  1165. if ($this->sortorder == $sortorder) {
  1166. return;
  1167. }
  1168. $this->sortorder = $sortorder;
  1169. $this->update();
  1170. }
  1171. /**
  1172. * Update this grade item's sortorder so that it will appear after $sortorder
  1173. *
  1174. * @param int $sortorder The sort order to place this grade item after
  1175. */
  1176. public function move_after_sortorder($sortorder) {
  1177. global $CFG, $DB;
  1178. //make some room first
  1179. $params = array($sortorder, $this->courseid);
  1180. $sql = "UPDATE {grade_items}
  1181. SET sortorder = sortorder + 1
  1182. WHERE sortorder > ? AND courseid = ?";
  1183. $DB->execute($sql, $params);
  1184. $this->set_sortorder($sortorder + 1);
  1185. }
  1186. /**
  1187. * Detect duplicate grade item's sortorder and re-sort them.
  1188. * Note: Duplicate sortorder will be introduced while duplicating activities or
  1189. * merging two courses.
  1190. *
  1191. * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
  1192. */
  1193. public static function fix_duplicate_sortorder($courseid) {
  1194. global $DB;
  1195. $transaction = $DB->start_delegated_transaction();
  1196. $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
  1197. FROM {grade_items} g1
  1198. JOIN {grade_items} g2 ON g1.courseid = g2.courseid
  1199. WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
  1200. ORDER BY g1.sortorder DESC, g1.id DESC";
  1201. // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
  1202. // bottom higher end of the sort orders and work down by id.
  1203. $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
  1204. foreach($rs as $duplicate) {
  1205. $DB->execute("UPDATE {grade_items}
  1206. SET sortorder = sortorder + 1
  1207. WHERE courseid = :courseid AND
  1208. (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
  1209. array('courseid' => $duplicate->courseid,
  1210. 'sortorder' => $duplicate->sortorder,
  1211. 'sortorder2' => $duplicate->sortorder,
  1212. 'id' => $duplicate->id));
  1213. }
  1214. $rs->close();
  1215. $transaction->allow_commit();
  1216. }
  1217. /**
  1218. * Returns the most descriptive field for this object.
  1219. *
  1220. * Determines what type of grade item it is then returns the appropriate string
  1221. *
  1222. * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
  1223. * @return string name
  1224. */
  1225. public function get_name($fulltotal=false) {
  1226. global $CFG;
  1227. require_once($CFG->dirroot . '/course/lib.php');
  1228. if (strval($this->itemname) !== '') {
  1229. // MDL-10557
  1230. // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
  1231. $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
  1232. $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
  1233. $options = ['context' => context_course::instance($this->courseid)];
  1234. return $deletionpending ?
  1235. format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
  1236. format_string($this->itemname, true, $options);
  1237. } else if ($this->is_course_item()) {
  1238. return get_string('coursetotal', 'grades');
  1239. } else if ($this->is_category_item()) {
  1240. if ($fulltotal) {
  1241. $category = $this->load_parent_category();
  1242. $a = new stdClass();
  1243. $a->category = $category->get_name();
  1244. return get_string('categorytotalfull', 'grades', $a);
  1245. } else {
  1246. return get_string('categorytotal', 'grades');
  1247. }
  1248. } else {
  1249. return get_string('grade');
  1250. }
  1251. }
  1252. /**
  1253. * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
  1254. *
  1255. * @return string description
  1256. */
  1257. public function get_description() {
  1258. if ($this->is_course_item() || $this->is_category_item()) {
  1259. $categoryitem = $this->load_item_category();
  1260. return $categoryitem->get_description();
  1261. }
  1262. return '';
  1263. }
  1264. /**
  1265. * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
  1266. *
  1267. * @param int $parentid The ID of the new parent
  1268. * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
  1269. * Set this to false when the aggregation fields have been updated in prevision of the new
  1270. * category, typically when the item is freshly created.
  1271. * @return bool True if success
  1272. */
  1273. public function set_parent($parentid, $updateaggregationfields = true) {
  1274. if ($this->is_course_item() or $this->is_category_item()) {
  1275. print_error('cannotsetparentforcatoritem');
  1276. }
  1277. if ($this->categoryid == $parentid) {
  1278. return true;
  1279. }
  1280. // find parent and check course id
  1281. if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
  1282. return false;
  1283. }
  1284. $currentparent = $this->load_parent_category();
  1285. if ($updateaggregationfields) {
  1286. $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
  1287. }
  1288. $this->force_regrading();
  1289. // set new parent
  1290. $this->categoryid = $parent_category->id;
  1291. $this->parent_category =& $parent_category;
  1292. return $this->update();
  1293. }
  1294. /**
  1295. * Update the aggregation fields when the aggregation changed.
  1296. *
  1297. * This method should always be called when the aggregation has changed, but also when
  1298. * the item was moved to another category, even it if uses the same aggregation method.
  1299. *
  1300. * Some values such as the weight only make sense within a category, once moved the
  1301. * values should be reset to let the user adapt them accordingly.
  1302. *
  1303. * Note that this method does not save the grade item.
  1304. * {@link grade_item::update()} has to be called manually after using this method.
  1305. *
  1306. * @param int $from Aggregation method constant value.
  1307. * @param int $to Aggregation method constant value.
  1308. * @return boolean True when at least one field was changed, false otherwise
  1309. */
  1310. public function set_aggregation_fields_for_aggregation($from, $to) {
  1311. $defaults = grade_category::get_default_aggregation_coefficient_values($to);
  1312. $origaggregationcoef = $this->aggregationcoef;
  1313. $origaggregationcoef2 = $this->aggregationcoef2;
  1314. $origweighoverride = $this->weightoverride;
  1315. if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
  1316. // Do nothing. We are switching from SUM to SUM and the weight is overriden,
  1317. // a teacher would not expect any change in this situation.
  1318. } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
  1319. // Do nothing. The weights can be kept in this case.
  1320. } else if (in_array($from, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
  1321. && in_array($to, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
  1322. // Reset all but the the extra credit field.
  1323. $this->aggregationcoef2 = $defaults['aggregationcoef2'];
  1324. $this->weightoverride = $defaults['weightoverride'];
  1325. if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
  1326. // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
  1327. $this->aggregationcoef = min(1, $this->aggregationcoef);
  1328. }
  1329. } else {
  1330. // Reset all.
  1331. $this->aggregationcoef = $defaults['aggregationcoef'];
  1332. $this->aggregationcoef2 = $defaults['aggregationcoef2'];
  1333. $this->weightoverride = $defaults['weightoverride'];
  1334. }
  1335. $acoefdiff = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
  1336. $acoefdiff2 = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
  1337. $weightoverride = grade_floats_different($origweighoverride, $this->weightoverride);
  1338. return $acoefdiff || $acoefdiff2 || $weightoverride;
  1339. }
  1340. /**
  1341. * Makes sure value is a valid grade value.
  1342. *
  1343. * @param float $gradevalue
  1344. * @return mixed float or int fixed grade value
  1345. */
  1346. public function bounded_grade($gradevalue) {
  1347. global $CFG;
  1348. if (is_null($gradevalue)) {
  1349. return null;
  1350. }
  1351. if ($this->gradetype == GRADE_TYPE_SCALE) {
  1352. // no >100% grades hack for scale grades!
  1353. // 1.5 is rounded to 2 ;-)
  1354. return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
  1355. }
  1356. $grademax = $this->grademax;
  1357. // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
  1358. $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
  1359. if (!empty($CFG->unlimitedgrades)) {
  1360. // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
  1361. $grademax = $grademax * $maxcoef;
  1362. } else if ($this->is_category_item() or $this->is_course_item()) {
  1363. $category = $this->load_item_category();
  1364. if ($category->aggregation >= 100) {
  1365. // grade >100% hack
  1366. $grademax = $grademax * $maxcoef;
  1367. }
  1368. }
  1369. return (float)bounded_number($this->grademin, $gradevalue, $grademax);
  1370. }
  1371. /**
  1372. * Finds out on which other items does this depend directly when doing calculation or category aggregation
  1373. *
  1374. * @param bool $reset_cache
  1375. * @return array of grade_item IDs this one depends on
  1376. */
  1377. public function depends_on($reset_cache=false) {
  1378. global $CFG, $DB;
  1379. if ($reset_cache) {
  1380. $this->dependson_cache = null;
  1381. } else if (isset($this->dependson_cache)) {
  1382. return $this->dependson_cache;
  1383. }
  1384. if ($this->is_locked()) {
  1385. // locked items do not need to be regraded
  1386. $this->dependson_cache = array();
  1387. return $this->dependson_cache;
  1388. }
  1389. if ($this->is_calculated()) {
  1390. if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
  1391. $this->dependson_cache = array_unique($matches[1]); // remove duplicates
  1392. return $this->dependson_cache;
  1393. } else {
  1394. $this->dependson_cache = array();
  1395. return $this->dependson_cache;
  1396. }
  1397. } else if ($grade_category = $this->load_item_category()) {
  1398. $params = array();
  1399. //only items with numeric or scale values can be aggregated
  1400. if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
  1401. $this->dependson_cache = array();
  1402. return $this->dependson_cache;
  1403. }
  1404. $grade_category->apply_forced_settings();
  1405. if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
  1406. $outcomes_sql = "";
  1407. } else {
  1408. $outcomes_sql = "AND gi.outcomeid IS NULL";
  1409. }
  1410. if (empty($CFG->grade_includescalesinaggregation)) {
  1411. $gtypes = "gi.gradetype = ?";
  1412. $params[] = GRADE_TYPE_VALUE;
  1413. } else {
  1414. $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
  1415. $params[] = GRADE_TYPE_VALUE;
  1416. $params[] = GRADE_TYPE_SCALE;
  1417. }
  1418. $params[] = $grade_category->id;
  1419. $params[] = $this->courseid;
  1420. $params[] = $grade_category->id;
  1421. $params[] = $this->courseid;
  1422. if (empty($CFG->grade_includescalesinaggregation)) {
  1423. $params[] = GRADE_TYPE_VALUE;
  1424. } else {
  1425. $params[] = GRADE_TYPE_VALUE;
  1426. $params[] = GRADE_TYPE_SCALE;
  1427. }
  1428. $sql = "SELECT gi.id
  1429. FROM {grade_items} gi
  1430. WHERE $gtypes
  1431. AND gi.categoryid = ?
  1432. AND gi.courseid = ?
  1433. $outcomes_sql
  1434. UNION
  1435. SELECT gi.id
  1436. FROM {grade_items} gi, {grade_categories} gc
  1437. WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
  1438. AND gc.parent = ?
  1439. AND gi.courseid = ?
  1440. AND $gtypes
  1441. $outcomes_sql";
  1442. if ($children = $DB->get_records_sql($sql, $params)) {
  1443. $this->dependson_cache = array_keys($children);
  1444. return $this->dependson_cache;
  1445. } else {
  1446. $this->dependson_cache = array();
  1447. return $this->dependson_cache;
  1448. }
  1449. } else {
  1450. $this->dependson_cache = array();
  1451. return $this->dependson_cache;
  1452. }
  1453. }
  1454. /**
  1455. * Refetch grades from modules, plugins.
  1456. *
  1457. * @param int $userid optional, limit the refetch to a single user
  1458. * @return bool Returns true on success or if there is nothing to do
  1459. */
  1460. public function refresh_grades($userid=0) {
  1461. global $DB;
  1462. if ($this->itemtype == 'mod') {
  1463. if ($this->is_outcome_item()) {
  1464. //nothing to do
  1465. return true;
  1466. }
  1467. if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
  1468. debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
  1469. return false;
  1470. }
  1471. if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
  1472. debugging('Can not find course module');
  1473. return false;
  1474. }
  1475. $activity->modname = $this->itemmodule;
  1476. $activity->cmidnumber = $cm->idnumber;
  1477. return grade_update_mod_grades($activity, $userid);
  1478. }
  1479. return true;
  1480. }
  1481. /**
  1482. * Updates final grade value for given user, this is a only way to update final
  1483. * grades from gradebook and import because it logs the change in history table
  1484. * and deals with overridden flag. This flag is set to prevent later overriding
  1485. * from raw grades submitted from modules.
  1486. *
  1487. * @param int $userid The graded user
  1488. * @param float|false $finalgrade The float value of final grade, false means do not change
  1489. * @param string $source The modification source
  1490. * @param string $feedback Optional teacher feedback
  1491. * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
  1492. * @param int $usermodified The ID of the user making the modification
  1493. * @param int $timemodified Optional parameter to set the time modified, if not present current time.
  1494. * @return bool success
  1495. */
  1496. public function update_final_grade($userid, $finalgrade = false,
  1497. $source = null, $feedback = false,
  1498. $feedbackformat = FORMAT_MOODLE,
  1499. $usermodified = null, $timemodified = null) {
  1500. global $USER, $CFG;
  1501. $result = true;
  1502. // no grading used or locked
  1503. if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
  1504. return false;
  1505. }
  1506. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
  1507. $grade->grade_item =& $this; // prevent db fetching of this grade_item
  1508. if (empty($usermodified)) {
  1509. $grade->usermodified = $USER->id;
  1510. } else {
  1511. $grade->usermodified = $usermodified;
  1512. }
  1513. if ($grade->is_locked()) {
  1514. // do not update locked grades at all
  1515. return false;
  1516. }
  1517. $locktime = $grade->get_locktime();
  1518. if ($locktime and $locktime < time()) {
  1519. // do not update grades that should be already locked, force regrade instead
  1520. $this->force_regrading();
  1521. return false;
  1522. }
  1523. $oldgrade = new stdClass();
  1524. $oldgrade->finalgrade = $grade->finalgrade;
  1525. $oldgrade->overridden = $grade->overridden;
  1526. $oldgrade->feedback = $grade->feedback;
  1527. $oldgrade->feedbackformat = $grade->feedbackformat;
  1528. $oldgrade->rawgrademin = $grade->rawgrademin;
  1529. $oldgrade->rawgrademax = $grade->rawgrademax;
  1530. // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
  1531. $grade->rawgrademin = $this->grademin;
  1532. $grade->rawgrademax = $this->grademax;
  1533. $grade->rawscaleid = $this->scaleid;
  1534. // changed grade?
  1535. if ($finalgrade !== false) {
  1536. if ($this->is_overridable_item()) {
  1537. $grade->overridden = time();
  1538. }
  1539. $grade->finalgrade = $this->bounded_grade($finalgrade);
  1540. }
  1541. // do we have comment from teacher?
  1542. if ($feedback !== false) {
  1543. if ($this->is_overridable_item_feedback()) {
  1544. // external items (modules, plugins) may have own feedback
  1545. $grade->overridden = time();
  1546. }
  1547. $grade->feedback = $feedback;
  1548. $grade->feedbackformat = $feedbackformat;
  1549. }
  1550. $gradechanged = false;
  1551. if (empty($grade->id)) {
  1552. $grade->timecreated = null; // Hack alert - date submitted - no submission yet.
  1553. $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
  1554. $result = (bool)$grade->insert($source);
  1555. // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
  1556. if ($result && !is_null($grade->finalgrade)) {
  1557. \core\event\user_graded::create_from_grade($grade)->trigger();
  1558. }
  1559. $gradechanged = true;
  1560. } else {
  1561. // Existing grade_grades.
  1562. if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
  1563. or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
  1564. or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
  1565. or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
  1566. $gradechanged = true;
  1567. }
  1568. if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
  1569. $gradechanged === false) {
  1570. // No grade nor feedback changed.
  1571. return $result;
  1572. }
  1573. $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
  1574. $result = $grade->update($source);
  1575. // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
  1576. if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
  1577. \core\event\user_graded::create_from_grade($grade)->trigger();
  1578. }
  1579. }
  1580. if (!$result) {
  1581. // Something went wrong - better force final grade recalculation.
  1582. $this->force_regrading();
  1583. return $result;
  1584. }
  1585. // If we are not updating grades we don't need to recalculate the whole course.
  1586. if (!$gradechanged) {
  1587. return $result;
  1588. }
  1589. if ($this->is_course_item() and !$this->needsupdate) {
  1590. if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
  1591. $this->force_regrading();
  1592. }
  1593. } else if (!$this->needsupdate) {
  1594. $course_item = grade_item::fetch_course_item($this->courseid);
  1595. if (!$course_item->needsupdate) {
  1596. if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
  1597. $this->force_regrading();
  1598. }
  1599. } else {
  1600. $this->force_regrading();
  1601. }
  1602. }
  1603. return $result;
  1604. }
  1605. /**
  1606. * Updates raw grade value for given user, this is a only way to update raw
  1607. * grades from external source (modules, etc.),
  1608. * because it logs the change in history table and deals with final grade recalculation.
  1609. *
  1610. * @param int $userid the graded user
  1611. * @param mixed $rawgrade float value of raw grade - false means do not change
  1612. * @param string $source modification source
  1613. * @param string $feedback optional teacher feedback
  1614. * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
  1615. * @param int $usermodified the ID of the user who did the grading
  1616. * @param int $dategraded A timestamp of when the student's work was graded
  1617. * @param int $datesubmitted A timestamp of when the student's work was submitted
  1618. * @param grade_grade $grade A grade object, useful for bulk upgrades
  1619. * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
  1620. * Example -
  1621. * [
  1622. * 'contextid' => 1,
  1623. * 'component' => 'mod_xyz',
  1624. * 'filearea' => 'mod_xyz_feedback',
  1625. * 'itemid' => 2
  1626. * ];
  1627. * @return bool success
  1628. */
  1629. public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
  1630. $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
  1631. $grade = null, array $feedbackfiles = []) {
  1632. global $USER;
  1633. $result = true;
  1634. // calculated grades can not be updated; course and category can not be updated because they are aggregated
  1635. if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
  1636. return false;
  1637. }
  1638. if (is_null($grade)) {
  1639. //fetch from db
  1640. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
  1641. }
  1642. $grade->grade_item =& $this; // prevent db fetching of this grade_item
  1643. if (empty($usermodified)) {
  1644. $grade->usermodified = $USER->id;
  1645. } else {
  1646. $grade->usermodified = $usermodified;
  1647. }
  1648. if ($grade->is_locked()) {
  1649. // do not update locked grades at all
  1650. return false;
  1651. }
  1652. $locktime = $grade->get_locktime();
  1653. if ($locktime and $locktime < time()) {
  1654. // do not update grades that should be already locked and force regrade
  1655. $this->force_regrading();
  1656. return false;
  1657. }
  1658. $oldgrade = new stdClass();
  1659. $oldgrade->finalgrade = $grade->finalgrade;
  1660. $oldgrade->rawgrade = $grade->rawgrade;
  1661. $oldgrade->rawgrademin = $grade->rawgrademin;
  1662. $oldgrade->rawgrademax = $grade->rawgrademax;
  1663. $oldgrade->rawscaleid = $grade->rawscaleid;
  1664. $oldgrade->feedback = $grade->feedback;
  1665. $oldgrade->feedbackformat = $grade->feedbackformat;
  1666. // use new min and max
  1667. $grade->rawgrade = $grade->rawgrade;
  1668. $grade->rawgrademin = $this->grademin;
  1669. $grade->rawgrademax = $this->grademax;
  1670. $grade->rawscaleid = $this->scaleid;
  1671. // change raw grade?
  1672. if ($rawgrade !== false) {
  1673. $grade->rawgrade = $rawgrade;
  1674. }
  1675. // empty feedback means no feedback at all
  1676. if ($feedback === '') {
  1677. $feedback = null;
  1678. }
  1679. // do we have comment from teacher?
  1680. if ($feedback !== false and !$grade->is_overridden()) {
  1681. $grade->feedback = $feedback;
  1682. $grade->feedbackformat = $feedbackformat;
  1683. $grade->feedbackfiles = $feedbackfiles;
  1684. }
  1685. // update final grade if possible
  1686. if (!$grade->is_locked() and !$grade->is_overridden()) {
  1687. $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  1688. }
  1689. // TODO: hack alert - create new fields for these in 2.0
  1690. $oldgrade->timecreated = $grade->timecreated;
  1691. $oldgrade->timemodified = $grade->timemodified;
  1692. $grade->timecreated = $datesubmitted;
  1693. if ($grade->is_overridden()) {
  1694. // keep original graded date - update_final_grade() sets this for overridden grades
  1695. } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
  1696. // no grade and feedback means no grading yet
  1697. $grade->timemodified = null;
  1698. } else if (!empty($dategraded)) {
  1699. // fine - module sends info when graded (yay!)
  1700. $grade->timemodified = $dategraded;
  1701. } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
  1702. or $grade->feedback !== $oldgrade->feedback) {
  1703. // guess - if either grade or feedback changed set new graded date
  1704. $grade->timemodified = time();
  1705. } else {
  1706. //keep original graded date
  1707. }
  1708. // end of hack alert
  1709. $gradechanged = false;
  1710. if (empty($grade->id)) {
  1711. $result = (bool)$grade->insert($source);
  1712. // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
  1713. if ($result && !is_null($grade->finalgrade)) {
  1714. \core\event\user_graded::create_from_grade($grade)->trigger();
  1715. }
  1716. $gradechanged = true;
  1717. } else {
  1718. // Existing grade_grades.
  1719. if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
  1720. or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)
  1721. or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
  1722. or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
  1723. or $grade->rawscaleid != $oldgrade->rawscaleid) {
  1724. $gradechanged = true;
  1725. }
  1726. // The timecreated and timemodified checking is part of the hack above.
  1727. if ($gradechanged === false and
  1728. $grade->feedback === $oldgrade->feedback and
  1729. $grade->feedbackformat == $oldgrade->feedbackformat and
  1730. $grade->timecreated == $oldgrade->timecreated and
  1731. $grade->timemodified == $oldgrade->timemodified) {
  1732. // No changes.
  1733. return $result;
  1734. }
  1735. $result = $grade->update($source);
  1736. // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
  1737. if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
  1738. \core\event\user_graded::create_from_grade($grade)->trigger();
  1739. }
  1740. }
  1741. if (!$result) {
  1742. // Something went wrong - better force final grade recalculation.
  1743. $this->force_regrading();
  1744. return $result;
  1745. }
  1746. // If we are not updating grades we don't need to recalculate the whole course.
  1747. if (!$gradechanged) {
  1748. return $result;
  1749. }
  1750. if (!$this->needsupdate) {
  1751. $course_item = grade_item::fetch_course_item($this->courseid);
  1752. if (!$course_item->needsupdate) {
  1753. if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
  1754. $this->force_regrading();
  1755. }
  1756. }
  1757. }
  1758. return $result;
  1759. }
  1760. /**
  1761. * Calculates final grade values using the formula in the calculation property.
  1762. * The parameters are taken from final grades of grade items in current course only.
  1763. *
  1764. * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
  1765. * @return bool false if error
  1766. */
  1767. public function compute($userid=null) {
  1768. global $CFG, $DB;
  1769. if (!$this->is_calculated()) {
  1770. return false;
  1771. }
  1772. require_once($CFG->libdir.'/mathslib.php');
  1773. if ($this->is_locked()) {
  1774. return true; // no need to recalculate locked items
  1775. }
  1776. // Precreate grades - we need them to exist
  1777. if ($userid) {
  1778. $missing = array();
  1779. if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
  1780. $m = new stdClass();
  1781. $m->userid = $userid;
  1782. $missing[] = $m;
  1783. }
  1784. } else {
  1785. // Find any users who have grades for some but not all grade items in this course
  1786. $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
  1787. $sql = "SELECT gg.userid
  1788. FROM {grade_grades} gg
  1789. JOIN {grade_items} gi
  1790. ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
  1791. GROUP BY gg.userid
  1792. HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
  1793. $missing = $DB->get_records_sql($sql, $params);
  1794. }
  1795. if ($missing) {
  1796. foreach ($missing as $m) {
  1797. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
  1798. $grade->grade_item =& $this;
  1799. $grade->insert('system');
  1800. }
  1801. }
  1802. // get used items
  1803. $useditems = $this->depends_on();
  1804. // prepare formula and init maths library
  1805. $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
  1806. if (strpos($formula, '[[') !== false) {
  1807. // missing item
  1808. return false;
  1809. }
  1810. $this->formula = new calc_formula($formula);
  1811. // where to look for final grades?
  1812. // this itemid is added so that we use only one query for source and final grades
  1813. $gis = array_merge($useditems, array($this->id));
  1814. list($usql, $params) = $DB->get_in_or_equal($gis);
  1815. if ($userid) {
  1816. $usersql = "AND g.userid=?";
  1817. $params[] = $userid;
  1818. } else {
  1819. $usersql = "";
  1820. }
  1821. $grade_inst = new grade_grade();
  1822. $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
  1823. $params[] = $this->courseid;
  1824. $sql = "SELECT $fields
  1825. FROM {grade_grades} g, {grade_items} gi
  1826. WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
  1827. ORDER BY g.userid";
  1828. $return = true;
  1829. // group the grades by userid and use formula on the group
  1830. $rs = $DB->get_recordset_sql($sql, $params);
  1831. if ($rs->valid()) {
  1832. $prevuser = 0;
  1833. $grade_records = array();
  1834. $oldgrade = null;
  1835. foreach ($rs as $used) {
  1836. if ($used->userid != $prevuser) {
  1837. if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
  1838. $return = false;
  1839. }
  1840. $prevuser = $used->userid;
  1841. $grade_records = array();
  1842. $oldgrade = null;
  1843. }
  1844. if ($used->itemid == $this->id) {
  1845. $oldgrade = $used;
  1846. }
  1847. $grade_records['gi'.$used->itemid] = $used->finalgrade;
  1848. }
  1849. if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
  1850. $return = false;
  1851. }
  1852. }
  1853. $rs->close();
  1854. return $return;
  1855. }
  1856. /**
  1857. * Internal function that does the final grade calculation
  1858. *
  1859. * @param int $userid The user ID
  1860. * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
  1861. * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
  1862. * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
  1863. * @return bool False if an error occurred
  1864. */
  1865. public function use_formula($userid, $params, $useditems, $oldgrade) {
  1866. if (empty($userid)) {
  1867. return true;
  1868. }
  1869. // add missing final grade values
  1870. // not graded (null) is counted as 0 - the spreadsheet way
  1871. $allinputsnull = true;
  1872. foreach($useditems as $gi) {
  1873. if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
  1874. $params['gi'.$gi] = 0;
  1875. } else {
  1876. $params['gi'.$gi] = (float)$params['gi'.$gi];
  1877. if ($gi != $this->id) {
  1878. $allinputsnull = false;
  1879. }
  1880. }
  1881. }
  1882. // can not use own final grade during calculation
  1883. unset($params['gi'.$this->id]);
  1884. // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
  1885. // wish to update the grades.
  1886. $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
  1887. $rawminandmaxchanged = false;
  1888. // insert final grade - will be needed later anyway
  1889. if ($oldgrade) {
  1890. // Only run through this code if the gradebook isn't frozen.
  1891. if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
  1892. // Do nothing.
  1893. } else {
  1894. // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
  1895. // grade_item grade maximum and minimum respectively.
  1896. if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
  1897. $rawminandmaxchanged = true;
  1898. $oldgrade->rawgrademax = $this->grademax;
  1899. $oldgrade->rawgrademin = $this->grademin;
  1900. }
  1901. }
  1902. $oldfinalgrade = $oldgrade->finalgrade;
  1903. $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
  1904. $grade->grade_item =& $this;
  1905. } else {
  1906. $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
  1907. $grade->grade_item =& $this;
  1908. $rawminandmaxchanged = false;
  1909. if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
  1910. // Do nothing.
  1911. } else {
  1912. // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
  1913. // grade_item grade maximum and minimum respectively.
  1914. $rawminandmaxchanged = true;
  1915. $grade->rawgrademax = $this->grademax;
  1916. $grade->rawgrademin = $this->grademin;
  1917. }
  1918. $grade->insert('system');
  1919. $oldfinalgrade = null;
  1920. }
  1921. // no need to recalculate locked or overridden grades
  1922. if ($grade->is_locked() or $grade->is_overridden()) {
  1923. return true;
  1924. }
  1925. if ($allinputsnull) {
  1926. $grade->finalgrade = null;
  1927. $result = true;
  1928. } else {
  1929. // do the calculation
  1930. $this->formula->set_params($params);
  1931. $result = $this->formula->evaluate();
  1932. if ($result === false) {
  1933. $grade->finalgrade = null;
  1934. } else {
  1935. // normalize
  1936. $grade->finalgrade = $this->bounded_grade($result);
  1937. }
  1938. }
  1939. // Only run through this code if the gradebook isn't frozen.
  1940. if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
  1941. // Update in db if changed.
  1942. if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
  1943. $grade->timemodified = time();
  1944. $success = $grade->update('compute');
  1945. // If successful trigger a user_graded event.
  1946. if ($success) {
  1947. \core\event\user_graded::create_from_grade($grade)->trigger();
  1948. }
  1949. }
  1950. } else {
  1951. // Update in db if changed.
  1952. if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
  1953. $grade->timemodified = time();
  1954. $success = $grade->update('compute');
  1955. // If successful trigger a user_graded event.
  1956. if ($success) {
  1957. \core\event\user_graded::create_from_grade($grade)->trigger();
  1958. }
  1959. }
  1960. }
  1961. if ($result !== false) {
  1962. //lock grade if needed
  1963. }
  1964. if ($result === false) {
  1965. return false;
  1966. } else {
  1967. return true;
  1968. }
  1969. }
  1970. /**
  1971. * Validate the formula.
  1972. *
  1973. * @param string $formulastr
  1974. * @return bool true if calculation possible, false otherwise
  1975. */
  1976. public function validate_formula($formulastr) {
  1977. global $CFG, $DB;
  1978. require_once($CFG->libdir.'/mathslib.php');
  1979. $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
  1980. if (empty($formulastr)) {
  1981. return true;
  1982. }
  1983. if (strpos($formulastr, '=') !== 0) {
  1984. return get_string('errorcalculationnoequal', 'grades');
  1985. }
  1986. // get used items
  1987. if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
  1988. $useditems = array_unique($matches[1]); // remove duplicates
  1989. } else {
  1990. $useditems = array();
  1991. }
  1992. // MDL-11902
  1993. // unset the value if formula is trying to reference to itself
  1994. // but array keys does not match itemid
  1995. if (!empty($this->id)) {
  1996. $useditems = array_diff($useditems, array($this->id));
  1997. //unset($useditems[$this->id]);
  1998. }
  1999. // prepare formula and init maths library
  2000. $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
  2001. $formula = new calc_formula($formula);
  2002. if (empty($useditems)) {
  2003. $grade_items = array();
  2004. } else {
  2005. list($usql, $params) = $DB->get_in_or_equal($useditems);
  2006. $params[] = $this->courseid;
  2007. $sql = "SELECT gi.*
  2008. FROM {grade_items} gi
  2009. WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
  2010. if (!$grade_items = $DB->get_records_sql($sql, $params)) {
  2011. $grade_items = array();
  2012. }
  2013. }
  2014. $params = array();
  2015. foreach ($useditems as $itemid) {
  2016. // make sure all grade items exist in this course
  2017. if (!array_key_exists($itemid, $grade_items)) {
  2018. return false;
  2019. }
  2020. // use max grade when testing formula, this should be ok in 99.9%
  2021. // division by 0 is one of possible problems
  2022. $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
  2023. }
  2024. // do the calculation
  2025. $formula->set_params($params);
  2026. $result = $formula->evaluate();
  2027. // false as result indicates some problem
  2028. if ($result === false) {
  2029. // TODO: add more error hints
  2030. return get_string('errorcalculationunknown', 'grades');
  2031. } else {
  2032. return true;
  2033. }
  2034. }
  2035. /**
  2036. * Returns the value of the display type
  2037. *
  2038. * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
  2039. *
  2040. * @return int Display type
  2041. */
  2042. public function get_displaytype() {
  2043. global $CFG;
  2044. if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
  2045. return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
  2046. } else {
  2047. return $this->display;
  2048. }
  2049. }
  2050. /**
  2051. * Returns the value of the decimals field
  2052. *
  2053. * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
  2054. *
  2055. * @return int Decimals (0 - 5)
  2056. */
  2057. public function get_decimals() {
  2058. global $CFG;
  2059. if (is_null($this->decimals)) {
  2060. return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
  2061. } else {
  2062. return $this->decimals;
  2063. }
  2064. }
  2065. /**
  2066. * Returns a string representing the range of grademin - grademax for this grade item.
  2067. *
  2068. * @param int $rangesdisplaytype
  2069. * @param int $rangesdecimalpoints
  2070. * @return string
  2071. */
  2072. function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
  2073. global $USER;
  2074. // Determine which display type to use for this average
  2075. if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
  2076. $displaytype = GRADE_DISPLAY_TYPE_REAL;
  2077. } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
  2078. $displaytype = $this->get_displaytype();
  2079. } else {
  2080. $displaytype = $rangesdisplaytype;
  2081. }
  2082. // Override grade_item setting if a display preference (not default) was set for the averages
  2083. if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
  2084. $decimalpoints = $this->get_decimals();
  2085. } else {
  2086. $decimalpoints = $rangesdecimalpoints;
  2087. }
  2088. if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
  2089. $grademin = "0 %";
  2090. $grademax = "100 %";
  2091. } else {
  2092. $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
  2093. $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
  2094. }
  2095. return $grademin.'&ndash;'. $grademax;
  2096. }
  2097. /**
  2098. * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
  2099. *
  2100. * @return string|false Returns the coefficient string of false is no coefficient is being used
  2101. */
  2102. public function get_coefstring() {
  2103. $parent_category = $this->load_parent_category();
  2104. if ($this->is_category_item()) {
  2105. $parent_category = $parent_category->load_parent_category();
  2106. }
  2107. if ($parent_category->is_aggregationcoef_used()) {
  2108. return $parent_category->get_coefstring();
  2109. } else {
  2110. return false;
  2111. }
  2112. }
  2113. /**
  2114. * Returns whether the grade item can control the visibility of the grades
  2115. *
  2116. * @return bool
  2117. */
  2118. public function can_control_visibility() {
  2119. if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
  2120. return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
  2121. }
  2122. return parent::can_control_visibility();
  2123. }
  2124. /**
  2125. * Used to notify the completion system (if necessary) that a user's grade
  2126. * has changed, and clear up a possible score cache.
  2127. *
  2128. * @param bool $deleted True if grade was actually deleted
  2129. */
  2130. protected function notify_changed($deleted) {
  2131. global $CFG;
  2132. // Condition code may cache the grades for conditional availability of
  2133. // modules or sections. (This code should use a hook for communication
  2134. // with plugin, but hooks are not implemented at time of writing.)
  2135. if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
  2136. \availability_grade\callbacks::grade_item_changed($this->courseid);
  2137. }
  2138. }
  2139. /**
  2140. * Helper function to get the accurate context for this grade column.
  2141. *
  2142. * @return context
  2143. */
  2144. public function get_context() {
  2145. if ($this->itemtype == 'mod') {
  2146. $modinfo = get_fast_modinfo($this->courseid);
  2147. // Sometimes the course module cache is out of date and needs to be rebuilt.
  2148. if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
  2149. rebuild_course_cache($this->courseid, true);
  2150. $modinfo = get_fast_modinfo($this->courseid);
  2151. }
  2152. // Even with a rebuilt cache the module does not exist. This means the
  2153. // database is in an invalid state - we will log an error and return
  2154. // the course context but the calling code should be updated.
  2155. if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
  2156. mtrace(get_string('moduleinstancedoesnotexist', 'error'));
  2157. $context = \context_course::instance($this->courseid);
  2158. } else {
  2159. $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
  2160. $context = \context_module::instance($cm->id);
  2161. }
  2162. } else {
  2163. $context = \context_course::instance($this->courseid);
  2164. }
  2165. return $context;
  2166. }
  2167. }