PageRenderTime 66ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/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

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

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Definition of a class to represent a grade item
  18. *
  19. * @package core_grades
  20. * @category grade
  21. * @copyright 2006 Nicolas Connault
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once('grade_object.php');
  26. /**
  27. * Class representing a grade item.
  28. *
  29. * It is responsible for handling its DB representation, modifying and returning its metadata.
  30. *
  31. * @package core_grades
  32. * @category grade
  33. * @copyright 2006 Nicolas Connault
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class grade_item extends grade_object {
  37. /**
  38. * DB Table (used by grade_object).
  39. * @var string $table
  40. */
  41. public $table = 'grade_items';
  42. /**
  43. * Array of required table fields, must start with 'id'.
  44. * @var array $required_fields
  45. */
  46. public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
  47. 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
  48. 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
  49. '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. *…

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