PageRenderTime 28ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/gradelib.php

https://github.com/dongsheng/moodle
PHP | 1647 lines | 1012 code | 234 blank | 401 comment | 229 complexity | 3e07626fa624dca40af2b5127354c9d7 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, GPL-3.0, Apache-2.0, LGPL-2.1

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. * Library of functions for gradebook - both public and internal
  18. *
  19. * @package core_grades
  20. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. global $CFG;
  25. /** Include essential files */
  26. require_once($CFG->libdir . '/grade/constants.php');
  27. require_once($CFG->libdir . '/grade/grade_category.php');
  28. require_once($CFG->libdir . '/grade/grade_item.php');
  29. require_once($CFG->libdir . '/grade/grade_grade.php');
  30. require_once($CFG->libdir . '/grade/grade_scale.php');
  31. require_once($CFG->libdir . '/grade/grade_outcome.php');
  32. /////////////////////////////////////////////////////////////////////
  33. ///// Start of public API for communication with modules/blocks /////
  34. /////////////////////////////////////////////////////////////////////
  35. /**
  36. * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
  37. * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
  38. * Missing property or key means does not change the existing value.
  39. *
  40. * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
  41. * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
  42. *
  43. * Manual, course or category items can not be updated by this function.
  44. *
  45. * @category grade
  46. * @param string $source Source of the grade such as 'mod/assignment'
  47. * @param int $courseid ID of course
  48. * @param string $itemtype Type of grade item. For example, mod or block
  49. * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
  50. * @param int $iteminstance Instance ID of graded item
  51. * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
  52. * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
  53. * @param mixed $itemdetails Object or array describing the grading item, NULL if no change
  54. * @param bool $isbulkupdate If bulk grade update is happening.
  55. * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
  56. */
  57. function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades = null,
  58. $itemdetails = null, $isbulkupdate = false) {
  59. global $USER, $CFG, $DB;
  60. // only following grade_item properties can be changed in this function
  61. $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
  62. // list of 10,5 numeric fields
  63. $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
  64. // grade item identification
  65. $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
  66. if (is_null($courseid) or is_null($itemtype)) {
  67. debugging('Missing courseid or itemtype');
  68. return GRADE_UPDATE_FAILED;
  69. }
  70. if (!$gradeitems = grade_item::fetch_all($params)) {
  71. // create a new one
  72. $gradeitem = false;
  73. } else if (count($gradeitems) == 1) {
  74. $gradeitem = reset($gradeitems);
  75. unset($gradeitems); // Release memory.
  76. } else {
  77. debugging('Found more than one grade item');
  78. return GRADE_UPDATE_MULTIPLE;
  79. }
  80. if (!empty($itemdetails['deleted'])) {
  81. if ($gradeitem) {
  82. if ($gradeitem->delete($source)) {
  83. return GRADE_UPDATE_OK;
  84. } else {
  85. return GRADE_UPDATE_FAILED;
  86. }
  87. }
  88. return GRADE_UPDATE_OK;
  89. }
  90. /// Create or update the grade_item if needed
  91. if (!$gradeitem) {
  92. if ($itemdetails) {
  93. $itemdetails = (array)$itemdetails;
  94. // grademin and grademax ignored when scale specified
  95. if (array_key_exists('scaleid', $itemdetails)) {
  96. if ($itemdetails['scaleid']) {
  97. unset($itemdetails['grademin']);
  98. unset($itemdetails['grademax']);
  99. }
  100. }
  101. foreach ($itemdetails as $k=>$v) {
  102. if (!in_array($k, $allowed)) {
  103. // ignore it
  104. continue;
  105. }
  106. if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
  107. // no grade item needed!
  108. return GRADE_UPDATE_OK;
  109. }
  110. $params[$k] = $v;
  111. }
  112. }
  113. $gradeitem = new grade_item($params);
  114. $gradeitem->insert(null, $isbulkupdate);
  115. } else {
  116. if ($gradeitem->is_locked()) {
  117. // no notice() here, test returned value instead!
  118. return GRADE_UPDATE_ITEM_LOCKED;
  119. }
  120. if ($itemdetails) {
  121. $itemdetails = (array)$itemdetails;
  122. $update = false;
  123. foreach ($itemdetails as $k=>$v) {
  124. if (!in_array($k, $allowed)) {
  125. // ignore it
  126. continue;
  127. }
  128. if (in_array($k, $floats)) {
  129. if (grade_floats_different($gradeitem->{$k}, $v)) {
  130. $gradeitem->{$k} = $v;
  131. $update = true;
  132. }
  133. } else {
  134. if ($gradeitem->{$k} != $v) {
  135. $gradeitem->{$k} = $v;
  136. $update = true;
  137. }
  138. }
  139. }
  140. if ($update) {
  141. $gradeitem->update(null, $isbulkupdate);
  142. }
  143. }
  144. }
  145. /// reset grades if requested
  146. if (!empty($itemdetails['reset'])) {
  147. $gradeitem->delete_all_grades('reset');
  148. return GRADE_UPDATE_OK;
  149. }
  150. /// Some extra checks
  151. // do we use grading?
  152. if ($gradeitem->gradetype == GRADE_TYPE_NONE) {
  153. return GRADE_UPDATE_OK;
  154. }
  155. // no grade submitted
  156. if (empty($grades)) {
  157. return GRADE_UPDATE_OK;
  158. }
  159. /// Finally start processing of grades
  160. if (is_object($grades)) {
  161. $grades = array($grades->userid=>$grades);
  162. } else {
  163. if (array_key_exists('userid', $grades)) {
  164. $grades = array($grades['userid']=>$grades);
  165. }
  166. }
  167. /// normalize and verify grade array
  168. foreach($grades as $k=>$g) {
  169. if (!is_array($g)) {
  170. $g = (array)$g;
  171. $grades[$k] = $g;
  172. }
  173. if (empty($g['userid']) or $k != $g['userid']) {
  174. debugging('Incorrect grade array index, must be user id! Grade ignored.');
  175. unset($grades[$k]);
  176. }
  177. }
  178. if (empty($grades)) {
  179. return GRADE_UPDATE_FAILED;
  180. }
  181. $count = count($grades);
  182. if ($count > 0 and $count < 200) {
  183. list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid');
  184. $params['gid'] = $gradeitem->id;
  185. $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
  186. } else {
  187. $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
  188. $params = array('gid' => $gradeitem->id);
  189. }
  190. $rs = $DB->get_recordset_sql($sql, $params);
  191. $failed = false;
  192. while (count($grades) > 0) {
  193. $gradegrade = null;
  194. $grade = null;
  195. foreach ($rs as $gd) {
  196. $userid = $gd->userid;
  197. if (!isset($grades[$userid])) {
  198. // this grade not requested, continue
  199. continue;
  200. }
  201. // existing grade requested
  202. $grade = $grades[$userid];
  203. $gradegrade = new grade_grade($gd, false);
  204. unset($grades[$userid]);
  205. break;
  206. }
  207. if (is_null($gradegrade)) {
  208. if (count($grades) == 0) {
  209. // No more grades to process.
  210. break;
  211. }
  212. $grade = reset($grades);
  213. $userid = $grade['userid'];
  214. $gradegrade = new grade_grade(array('itemid' => $gradeitem->id, 'userid' => $userid), false);
  215. $gradegrade->load_optional_fields(); // add feedback and info too
  216. unset($grades[$userid]);
  217. }
  218. $rawgrade = false;
  219. $feedback = false;
  220. $feedbackformat = FORMAT_MOODLE;
  221. $feedbackfiles = [];
  222. $usermodified = $USER->id;
  223. $datesubmitted = null;
  224. $dategraded = null;
  225. if (array_key_exists('rawgrade', $grade)) {
  226. $rawgrade = $grade['rawgrade'];
  227. }
  228. if (array_key_exists('feedback', $grade)) {
  229. $feedback = $grade['feedback'];
  230. }
  231. if (array_key_exists('feedbackformat', $grade)) {
  232. $feedbackformat = $grade['feedbackformat'];
  233. }
  234. if (array_key_exists('feedbackfiles', $grade)) {
  235. $feedbackfiles = $grade['feedbackfiles'];
  236. }
  237. if (array_key_exists('usermodified', $grade)) {
  238. $usermodified = $grade['usermodified'];
  239. }
  240. if (array_key_exists('datesubmitted', $grade)) {
  241. $datesubmitted = $grade['datesubmitted'];
  242. }
  243. if (array_key_exists('dategraded', $grade)) {
  244. $dategraded = $grade['dategraded'];
  245. }
  246. // update or insert the grade
  247. if (!$gradeitem->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified,
  248. $dategraded, $datesubmitted, $gradegrade, $feedbackfiles, $isbulkupdate)) {
  249. $failed = true;
  250. }
  251. }
  252. if ($rs) {
  253. $rs->close();
  254. }
  255. if (!$failed) {
  256. return GRADE_UPDATE_OK;
  257. } else {
  258. return GRADE_UPDATE_FAILED;
  259. }
  260. }
  261. /**
  262. * Updates a user's outcomes. Manual outcomes can not be updated.
  263. *
  264. * @category grade
  265. * @param string $source Source of the grade such as 'mod/assignment'
  266. * @param int $courseid ID of course
  267. * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
  268. * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
  269. * @param int $iteminstance Instance ID of graded item. For example the forum ID.
  270. * @param int $userid ID of the graded user
  271. * @param array $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
  272. * @return bool returns true if grade items were found and updated successfully
  273. */
  274. function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
  275. if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
  276. $result = true;
  277. foreach ($items as $item) {
  278. if (!array_key_exists($item->itemnumber, $data)) {
  279. continue;
  280. }
  281. $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
  282. $result = ($item->update_final_grade($userid, $grade, $source) && $result);
  283. }
  284. return $result;
  285. }
  286. return false; //grade items not found
  287. }
  288. /**
  289. * Return true if the course needs regrading.
  290. *
  291. * @param int $courseid The course ID
  292. * @return bool true if course grades need updating.
  293. */
  294. function grade_needs_regrade_final_grades($courseid) {
  295. $course_item = grade_item::fetch_course_item($courseid);
  296. return $course_item->needsupdate;
  297. }
  298. /**
  299. * Return true if the regrade process is likely to be time consuming and
  300. * will therefore require the progress bar.
  301. *
  302. * @param int $courseid The course ID
  303. * @return bool Whether the regrade process is likely to be time consuming
  304. */
  305. function grade_needs_regrade_progress_bar($courseid) {
  306. global $DB;
  307. $grade_items = grade_item::fetch_all(array('courseid' => $courseid));
  308. list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED, 'gi');
  309. $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
  310. // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
  311. // Any longer than this and we want to show the progress bar.
  312. return $gradecount > 100;
  313. }
  314. /**
  315. * Check whether regarding of final grades is required and, if so, perform the regrade.
  316. *
  317. * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
  318. * function will output the progress bar, and redirect to the current PAGE->url after regrading
  319. * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
  320. * normal.
  321. *
  322. * A callback may be specified, which is called if regrading has taken place.
  323. * The callback may optionally return a URL which will be redirected to when the progress bar is present.
  324. *
  325. * @param stdClass $course The course to regrade
  326. * @param callable $callback A function to call if regrading took place
  327. * @return moodle_url The URL to redirect to if redirecting
  328. */
  329. function grade_regrade_final_grades_if_required($course, callable $callback = null) {
  330. global $PAGE, $OUTPUT;
  331. if (!grade_needs_regrade_final_grades($course->id)) {
  332. return false;
  333. }
  334. if (grade_needs_regrade_progress_bar($course->id)) {
  335. $PAGE->set_heading($course->fullname);
  336. echo $OUTPUT->header();
  337. echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
  338. $progress = new \core\progress\display(true);
  339. $status = grade_regrade_final_grades($course->id, null, null, $progress);
  340. // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
  341. if (is_array($status)) {
  342. foreach ($status as $error) {
  343. $errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
  344. echo $OUTPUT->render($errortext);
  345. }
  346. $courseitem = grade_item::fetch_course_item($course->id);
  347. $courseitem->regrading_finished();
  348. }
  349. if ($callback) {
  350. //
  351. $url = call_user_func($callback);
  352. }
  353. if (empty($url)) {
  354. $url = $PAGE->url;
  355. }
  356. echo $OUTPUT->continue_button($url);
  357. echo $OUTPUT->footer();
  358. die();
  359. } else {
  360. $result = grade_regrade_final_grades($course->id);
  361. if ($callback) {
  362. call_user_func($callback);
  363. }
  364. return $result;
  365. }
  366. }
  367. /**
  368. * Returns grading information for given activity, optionally with user grades
  369. * Manual, course or category items can not be queried.
  370. *
  371. * @category grade
  372. * @param int $courseid ID of course
  373. * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
  374. * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
  375. * @param int $iteminstance ID of the item module
  376. * @param mixed $userid_or_ids Either a single user ID, an array of user IDs or null. If user ID or IDs are not supplied returns information about grade_item
  377. * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
  378. */
  379. function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
  380. global $CFG;
  381. $return = new stdClass();
  382. $return->items = array();
  383. $return->outcomes = array();
  384. $course_item = grade_item::fetch_course_item($courseid);
  385. $needsupdate = array();
  386. if ($course_item->needsupdate) {
  387. $result = grade_regrade_final_grades($courseid);
  388. if ($result !== true) {
  389. $needsupdate = array_keys($result);
  390. }
  391. }
  392. if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
  393. foreach ($grade_items as $grade_item) {
  394. $decimalpoints = null;
  395. if (empty($grade_item->outcomeid)) {
  396. // prepare information about grade item
  397. $item = new stdClass();
  398. $item->id = $grade_item->id;
  399. $item->itemnumber = $grade_item->itemnumber;
  400. $item->itemtype = $grade_item->itemtype;
  401. $item->itemmodule = $grade_item->itemmodule;
  402. $item->iteminstance = $grade_item->iteminstance;
  403. $item->scaleid = $grade_item->scaleid;
  404. $item->name = $grade_item->get_name();
  405. $item->grademin = $grade_item->grademin;
  406. $item->grademax = $grade_item->grademax;
  407. $item->gradepass = $grade_item->gradepass;
  408. $item->locked = $grade_item->is_locked();
  409. $item->hidden = $grade_item->is_hidden();
  410. $item->grades = array();
  411. switch ($grade_item->gradetype) {
  412. case GRADE_TYPE_NONE:
  413. break;
  414. case GRADE_TYPE_VALUE:
  415. $item->scaleid = 0;
  416. break;
  417. case GRADE_TYPE_TEXT:
  418. $item->scaleid = 0;
  419. $item->grademin = 0;
  420. $item->grademax = 0;
  421. $item->gradepass = 0;
  422. break;
  423. }
  424. if (empty($userid_or_ids)) {
  425. $userids = array();
  426. } else if (is_array($userid_or_ids)) {
  427. $userids = $userid_or_ids;
  428. } else {
  429. $userids = array($userid_or_ids);
  430. }
  431. if ($userids) {
  432. $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
  433. foreach ($userids as $userid) {
  434. $grade_grades[$userid]->grade_item =& $grade_item;
  435. $grade = new stdClass();
  436. $grade->grade = $grade_grades[$userid]->finalgrade;
  437. $grade->locked = $grade_grades[$userid]->is_locked();
  438. $grade->hidden = $grade_grades[$userid]->is_hidden();
  439. $grade->overridden = $grade_grades[$userid]->overridden;
  440. $grade->feedback = $grade_grades[$userid]->feedback;
  441. $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
  442. $grade->usermodified = $grade_grades[$userid]->usermodified;
  443. $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
  444. $grade->dategraded = $grade_grades[$userid]->get_dategraded();
  445. // create text representation of grade
  446. if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
  447. $grade->grade = null;
  448. $grade->str_grade = '-';
  449. $grade->str_long_grade = $grade->str_grade;
  450. } else if (in_array($grade_item->id, $needsupdate)) {
  451. $grade->grade = false;
  452. $grade->str_grade = get_string('error');
  453. $grade->str_long_grade = $grade->str_grade;
  454. } else if (is_null($grade->grade)) {
  455. $grade->str_grade = '-';
  456. $grade->str_long_grade = $grade->str_grade;
  457. } else {
  458. $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
  459. if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
  460. $grade->str_long_grade = $grade->str_grade;
  461. } else {
  462. $a = new stdClass();
  463. $a->grade = $grade->str_grade;
  464. $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
  465. $grade->str_long_grade = get_string('gradelong', 'grades', $a);
  466. }
  467. }
  468. // create html representation of feedback
  469. if (is_null($grade->feedback)) {
  470. $grade->str_feedback = '';
  471. } else {
  472. $feedback = file_rewrite_pluginfile_urls(
  473. $grade->feedback,
  474. 'pluginfile.php',
  475. $grade_grades[$userid]->get_context()->id,
  476. GRADE_FILE_COMPONENT,
  477. GRADE_FEEDBACK_FILEAREA,
  478. $grade_grades[$userid]->id
  479. );
  480. $grade->str_feedback = format_text($feedback, $grade->feedbackformat,
  481. ['context' => $grade_grades[$userid]->get_context()]);
  482. }
  483. $item->grades[$userid] = $grade;
  484. }
  485. }
  486. $return->items[$grade_item->itemnumber] = $item;
  487. } else {
  488. if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
  489. debugging('Incorect outcomeid found');
  490. continue;
  491. }
  492. // outcome info
  493. $outcome = new stdClass();
  494. $outcome->id = $grade_item->id;
  495. $outcome->itemnumber = $grade_item->itemnumber;
  496. $outcome->itemtype = $grade_item->itemtype;
  497. $outcome->itemmodule = $grade_item->itemmodule;
  498. $outcome->iteminstance = $grade_item->iteminstance;
  499. $outcome->scaleid = $grade_outcome->scaleid;
  500. $outcome->name = $grade_outcome->get_name();
  501. $outcome->locked = $grade_item->is_locked();
  502. $outcome->hidden = $grade_item->is_hidden();
  503. if (empty($userid_or_ids)) {
  504. $userids = array();
  505. } else if (is_array($userid_or_ids)) {
  506. $userids = $userid_or_ids;
  507. } else {
  508. $userids = array($userid_or_ids);
  509. }
  510. if ($userids) {
  511. $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
  512. foreach ($userids as $userid) {
  513. $grade_grades[$userid]->grade_item =& $grade_item;
  514. $grade = new stdClass();
  515. $grade->grade = $grade_grades[$userid]->finalgrade;
  516. $grade->locked = $grade_grades[$userid]->is_locked();
  517. $grade->hidden = $grade_grades[$userid]->is_hidden();
  518. $grade->feedback = $grade_grades[$userid]->feedback;
  519. $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
  520. $grade->usermodified = $grade_grades[$userid]->usermodified;
  521. $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
  522. $grade->dategraded = $grade_grades[$userid]->get_dategraded();
  523. // create text representation of grade
  524. if (in_array($grade_item->id, $needsupdate)) {
  525. $grade->grade = false;
  526. $grade->str_grade = get_string('error');
  527. } else if (is_null($grade->grade)) {
  528. $grade->grade = 0;
  529. $grade->str_grade = get_string('nooutcome', 'grades');
  530. } else {
  531. $grade->grade = (int)$grade->grade;
  532. $scale = $grade_item->load_scale();
  533. $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
  534. }
  535. // create html representation of feedback
  536. if (is_null($grade->feedback)) {
  537. $grade->str_feedback = '';
  538. } else {
  539. $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
  540. }
  541. $outcome->grades[$userid] = $grade;
  542. }
  543. }
  544. if (isset($return->outcomes[$grade_item->itemnumber])) {
  545. // itemnumber duplicates - lets fix them!
  546. $newnumber = $grade_item->itemnumber + 1;
  547. while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
  548. $newnumber++;
  549. }
  550. $outcome->itemnumber = $newnumber;
  551. $grade_item->itemnumber = $newnumber;
  552. $grade_item->update('system');
  553. }
  554. $return->outcomes[$grade_item->itemnumber] = $outcome;
  555. }
  556. }
  557. }
  558. // sort results using itemnumbers
  559. ksort($return->items, SORT_NUMERIC);
  560. ksort($return->outcomes, SORT_NUMERIC);
  561. return $return;
  562. }
  563. ///////////////////////////////////////////////////////////////////
  564. ///// End of public API for communication with modules/blocks /////
  565. ///////////////////////////////////////////////////////////////////
  566. ///////////////////////////////////////////////////////////////////
  567. ///// Internal API: used by gradebook plugins and Moodle core /////
  568. ///////////////////////////////////////////////////////////////////
  569. /**
  570. * Returns a course gradebook setting
  571. *
  572. * @param int $courseid
  573. * @param string $name of setting, maybe null if reset only
  574. * @param string $default value to return if setting is not found
  575. * @param bool $resetcache force reset of internal static cache
  576. * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
  577. */
  578. function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
  579. global $DB;
  580. $cache = cache::make('core', 'gradesetting');
  581. $gradesetting = $cache->get($courseid) ?: array();
  582. if ($resetcache or empty($gradesetting)) {
  583. $gradesetting = array();
  584. $cache->set($courseid, $gradesetting);
  585. } else if (is_null($name)) {
  586. return null;
  587. } else if (array_key_exists($name, $gradesetting)) {
  588. return $gradesetting[$name];
  589. }
  590. if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
  591. $result = null;
  592. } else {
  593. $result = $data->value;
  594. }
  595. if (is_null($result)) {
  596. $result = $default;
  597. }
  598. $gradesetting[$name] = $result;
  599. $cache->set($courseid, $gradesetting);
  600. return $result;
  601. }
  602. /**
  603. * Returns all course gradebook settings as object properties
  604. *
  605. * @param int $courseid
  606. * @return object
  607. */
  608. function grade_get_settings($courseid) {
  609. global $DB;
  610. $settings = new stdClass();
  611. $settings->id = $courseid;
  612. if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
  613. foreach ($records as $record) {
  614. $settings->{$record->name} = $record->value;
  615. }
  616. }
  617. return $settings;
  618. }
  619. /**
  620. * Add, update or delete a course gradebook setting
  621. *
  622. * @param int $courseid The course ID
  623. * @param string $name Name of the setting
  624. * @param string $value Value of the setting. NULL means delete the setting.
  625. */
  626. function grade_set_setting($courseid, $name, $value) {
  627. global $DB;
  628. if (is_null($value)) {
  629. $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
  630. } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
  631. $data = new stdClass();
  632. $data->courseid = $courseid;
  633. $data->name = $name;
  634. $data->value = $value;
  635. $DB->insert_record('grade_settings', $data);
  636. } else {
  637. $data = new stdClass();
  638. $data->id = $existing->id;
  639. $data->value = $value;
  640. $DB->update_record('grade_settings', $data);
  641. }
  642. grade_get_setting($courseid, null, null, true); // reset the cache
  643. }
  644. /**
  645. * Returns string representation of grade value
  646. *
  647. * @param float $value The grade value
  648. * @param object $grade_item Grade item object passed by reference to prevent scale reloading
  649. * @param bool $localized use localised decimal separator
  650. * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
  651. * @param int $decimals The number of decimal places when displaying float values
  652. * @return string
  653. */
  654. function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
  655. if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
  656. return '';
  657. }
  658. // no grade yet?
  659. if (is_null($value)) {
  660. return '-';
  661. }
  662. if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
  663. //unknown type??
  664. return '';
  665. }
  666. if (is_null($displaytype)) {
  667. $displaytype = $grade_item->get_displaytype();
  668. }
  669. if (is_null($decimals)) {
  670. $decimals = $grade_item->get_decimals();
  671. }
  672. switch ($displaytype) {
  673. case GRADE_DISPLAY_TYPE_REAL:
  674. return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
  675. case GRADE_DISPLAY_TYPE_PERCENTAGE:
  676. return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
  677. case GRADE_DISPLAY_TYPE_LETTER:
  678. return grade_format_gradevalue_letter($value, $grade_item);
  679. case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
  680. return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
  681. grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
  682. case GRADE_DISPLAY_TYPE_REAL_LETTER:
  683. return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
  684. grade_format_gradevalue_letter($value, $grade_item) . ')';
  685. case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
  686. return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
  687. grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
  688. case GRADE_DISPLAY_TYPE_LETTER_REAL:
  689. return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
  690. grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
  691. case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
  692. return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
  693. grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
  694. case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
  695. return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
  696. grade_format_gradevalue_letter($value, $grade_item) . ')';
  697. default:
  698. return '';
  699. }
  700. }
  701. /**
  702. * Returns a float representation of a grade value
  703. *
  704. * @param float $value The grade value
  705. * @param object $grade_item Grade item object
  706. * @param int $decimals The number of decimal places
  707. * @param bool $localized use localised decimal separator
  708. * @return string
  709. */
  710. function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
  711. if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
  712. if (!$scale = $grade_item->load_scale()) {
  713. return get_string('error');
  714. }
  715. $value = $grade_item->bounded_grade($value);
  716. return format_string($scale->scale_items[$value-1]);
  717. } else {
  718. return format_float($value, $decimals, $localized);
  719. }
  720. }
  721. /**
  722. * Returns a percentage representation of a grade value
  723. *
  724. * @param float $value The grade value
  725. * @param object $grade_item Grade item object
  726. * @param int $decimals The number of decimal places
  727. * @param bool $localized use localised decimal separator
  728. * @return string
  729. */
  730. function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
  731. $min = $grade_item->grademin;
  732. $max = $grade_item->grademax;
  733. if ($min == $max) {
  734. return '';
  735. }
  736. $value = $grade_item->bounded_grade($value);
  737. $percentage = (($value-$min)*100)/($max-$min);
  738. return format_float($percentage, $decimals, $localized).' %';
  739. }
  740. /**
  741. * Returns a letter grade representation of a grade value
  742. * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
  743. *
  744. * @param float $value The grade value
  745. * @param object $grade_item Grade item object
  746. * @return string
  747. */
  748. function grade_format_gradevalue_letter($value, $grade_item) {
  749. global $CFG;
  750. $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
  751. if (!$letters = grade_get_letters($context)) {
  752. return ''; // no letters??
  753. }
  754. if (is_null($value)) {
  755. return '-';
  756. }
  757. $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
  758. $value = bounded_number(0, $value, 100); // just in case
  759. $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
  760. foreach ($letters as $boundary => $letter) {
  761. if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
  762. // Do nothing.
  763. } else {
  764. // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
  765. $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
  766. }
  767. if ($value >= $boundary) {
  768. return format_string($letter);
  769. }
  770. }
  771. return '-'; // no match? maybe '' would be more correct
  772. }
  773. /**
  774. * Returns grade options for gradebook grade category menu
  775. *
  776. * @param int $courseid The course ID
  777. * @param bool $includenew Include option for new category at array index -1
  778. * @return array of grade categories in course
  779. */
  780. function grade_get_categories_menu($courseid, $includenew=false) {
  781. $result = array();
  782. if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
  783. //make sure course category exists
  784. if (!grade_category::fetch_course_category($courseid)) {
  785. debugging('Can not create course grade category!');
  786. return $result;
  787. }
  788. $categories = grade_category::fetch_all(array('courseid'=>$courseid));
  789. }
  790. foreach ($categories as $key=>$category) {
  791. if ($category->is_course_category()) {
  792. $result[$category->id] = get_string('uncategorised', 'grades');
  793. unset($categories[$key]);
  794. }
  795. }
  796. if ($includenew) {
  797. $result[-1] = get_string('newcategory', 'grades');
  798. }
  799. $cats = array();
  800. foreach ($categories as $category) {
  801. $cats[$category->id] = $category->get_name();
  802. }
  803. core_collator::asort($cats);
  804. return ($result+$cats);
  805. }
  806. /**
  807. * Returns the array of grade letters to be used in the supplied context
  808. *
  809. * @param object $context Context object or null for defaults
  810. * @return array of grade_boundary (minimum) => letter_string
  811. */
  812. function grade_get_letters($context=null) {
  813. global $DB;
  814. if (empty($context)) {
  815. //default grading letters
  816. return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
  817. }
  818. $cache = cache::make('core', 'grade_letters');
  819. $data = $cache->get($context->id);
  820. if (!empty($data)) {
  821. return $data;
  822. }
  823. $letters = array();
  824. $contexts = $context->get_parent_context_ids();
  825. array_unshift($contexts, $context->id);
  826. foreach ($contexts as $ctxid) {
  827. if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
  828. foreach ($records as $record) {
  829. $letters[$record->lowerboundary] = $record->letter;
  830. }
  831. }
  832. if (!empty($letters)) {
  833. // Cache the grade letters for this context.
  834. $cache->set($context->id, $letters);
  835. return $letters;
  836. }
  837. }
  838. $letters = grade_get_letters(null);
  839. // Cache the grade letters for this context.
  840. $cache->set($context->id, $letters);
  841. return $letters;
  842. }
  843. /**
  844. * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
  845. *
  846. * @param string $idnumber string (with magic quotes)
  847. * @param int $courseid ID numbers are course unique only
  848. * @param grade_item $grade_item The grade item this idnumber is associated with
  849. * @param stdClass $cm used for course module idnumbers and items attached to modules
  850. * @return bool true means idnumber ok
  851. */
  852. function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
  853. global $DB;
  854. if ($idnumber == '') {
  855. //we allow empty idnumbers
  856. return true;
  857. }
  858. // keep existing even when not unique
  859. if ($cm and $cm->idnumber == $idnumber) {
  860. if ($grade_item and $grade_item->itemnumber != 0) {
  861. // grade item with itemnumber > 0 can't have the same idnumber as the main
  862. // itemnumber 0 which is synced with course_modules
  863. return false;
  864. }
  865. return true;
  866. } else if ($grade_item and $grade_item->idnumber == $idnumber) {
  867. return true;
  868. }
  869. if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
  870. return false;
  871. }
  872. if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
  873. return false;
  874. }
  875. return true;
  876. }
  877. /**
  878. * Force final grade recalculation in all course items
  879. *
  880. * @param int $courseid The course ID to recalculate
  881. */
  882. function grade_force_full_regrading($courseid) {
  883. global $DB;
  884. $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
  885. }
  886. /**
  887. * Forces regrading of all site grades. Used when changing site setings
  888. */
  889. function grade_force_site_regrading() {
  890. global $CFG, $DB;
  891. $DB->set_field('grade_items', 'needsupdate', 1);
  892. }
  893. /**
  894. * Recover a user's grades from grade_grades_history
  895. * @param int $userid the user ID whose grades we want to recover
  896. * @param int $courseid the relevant course
  897. * @return bool true if successful or false if there was an error or no grades could be recovered
  898. */
  899. function grade_recover_history_grades($userid, $courseid) {
  900. global $CFG, $DB;
  901. if ($CFG->disablegradehistory) {
  902. debugging('Attempting to recover grades when grade history is disabled.');
  903. return false;
  904. }
  905. //Were grades recovered? Flag to return.
  906. $recoveredgrades = false;
  907. //Check the user is enrolled in this course
  908. //Dont bother checking if they have a gradeable role. They may get one later so recover
  909. //whatever grades they have now just in case.
  910. $course_context = context_course::instance($courseid);
  911. if (!is_enrolled($course_context, $userid)) {
  912. debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
  913. return false;
  914. }
  915. //Check for existing grades for this user in this course
  916. //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
  917. //In the future we could move the existing grades to the history table then recover the grades from before then
  918. $sql = "SELECT gg.id
  919. FROM {grade_grades} gg
  920. JOIN {grade_items} gi ON gi.id = gg.itemid
  921. WHERE gi.courseid = :courseid AND gg.userid = :userid";
  922. $params = array('userid' => $userid, 'courseid' => $courseid);
  923. if ($DB->record_exists_sql($sql, $params)) {
  924. debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
  925. return false;
  926. } else {
  927. //Retrieve the user's old grades
  928. //have history ID as first column to guarantee we a unique first column
  929. $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
  930. h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
  931. h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
  932. FROM {grade_grades_history} h
  933. JOIN (SELECT itemid, MAX(id) AS id
  934. FROM {grade_grades_history}
  935. WHERE userid = :userid1
  936. GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
  937. JOIN {grade_items} gi ON gi.id = h.itemid
  938. JOIN (SELECT itemid, MAX(timemodified) AS tm
  939. FROM {grade_grades_history}
  940. WHERE userid = :userid2 AND action = :insertaction
  941. GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
  942. WHERE gi.courseid = :courseid";
  943. $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
  944. $oldgrades = $DB->get_records_sql($sql, $params);
  945. //now move the old grades to the grade_grades table
  946. foreach ($oldgrades as $oldgrade) {
  947. unset($oldgrade->id);
  948. $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
  949. $grade->insert($oldgrade->source);
  950. //dont include default empty grades created when activities are created
  951. if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
  952. $recoveredgrades = true;
  953. }
  954. }
  955. }
  956. //Some activities require manual grade synching (moving grades from the activity into the gradebook)
  957. //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
  958. grade_grab_course_grades($courseid, null, $userid);
  959. return $recoveredgrades;
  960. }
  961. /**
  962. * Updates all final grades in course.
  963. *
  964. * @param int $courseid The course ID
  965. * @param int $userid If specified try to do a quick regrading of the grades of this user only
  966. * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
  967. * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
  968. * @return bool true if ok, array of errors if problems found. Grade item id => error message
  969. */
  970. function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
  971. // This may take a very long time and extra memory.
  972. \core_php_time_limit::raise();
  973. raise_memory_limit(MEMORY_EXTRA);
  974. $course_item = grade_item::fetch_course_item($courseid);
  975. if ($progress == null) {
  976. $progress = new \core\progress\none();
  977. }
  978. if ($userid) {
  979. // one raw grade updated for one user
  980. if (empty($updated_item)) {
  981. print_error("cannotbenull", 'debug', '', "updated_item");
  982. }
  983. if ($course_item->needsupdate) {
  984. $updated_item->force_regrading();
  985. return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
  986. }
  987. } else {
  988. if (!$course_item->needsupdate) {
  989. // nothing to do :-)
  990. return true;
  991. }
  992. }
  993. // Categories might have to run some processing before we fetch the grade items.
  994. // This gives them a final opportunity to update and mark their children to be updated.
  995. // We need to work on the children categories up to the parent ones, so that, for instance,
  996. // if a category total is updated it will be reflected in the parent category.
  997. $cats = grade_category::fetch_all(array('courseid' => $courseid));
  998. $flatcattree = array();
  999. foreach ($cats as $cat) {
  1000. if (!isset($flatcattree[$cat->depth])) {
  1001. $flatcattree[$cat->depth] = array();
  1002. }
  1003. $flatcattree[$cat->depth][] = $cat;
  1004. }
  1005. krsort($flatcattree);
  1006. foreach ($flatcattree as $depth => $cats) {
  1007. foreach ($cats as $cat) {
  1008. $cat->pre_regrade_final_grades();
  1009. }
  1010. }
  1011. $progresstotal = 0;
  1012. $progresscurrent = 0;
  1013. $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
  1014. $depends_on = array();
  1015. foreach ($grade_items as $gid=>$gitem) {
  1016. if ((!empty($updated_item) and $updated_item->id == $gid) ||
  1017. $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
  1018. $grade_items[$gid]->needsupdate = 1;
  1019. }
  1020. // We load all dependencies of these items later we can discard some grade_items based on this.
  1021. if ($grade_items[$gid]->needsupdate) {
  1022. $depends_on[$gid] = $grade_items[$gid]->depends_on();
  1023. $progresstotal++;
  1024. }
  1025. }
  1026. $progress->start_progress('regrade_course', $progresstotal);
  1027. $errors = array();
  1028. $finalids = array();
  1029. $updatedids = array();
  1030. $gids = array_keys($grade_items);
  1031. $failed = 0;
  1032. while (count($finalids) < count($gids)) { // work until all grades are final or error found
  1033. $count = 0;
  1034. foreach ($gids as $gid) {
  1035. if (in_array($gid, $finalids)) {
  1036. continue; // already final
  1037. }
  1038. if (!$grade_items[$gid]->needsupdate) {
  1039. $finalids[] = $gid; // we can make it final - does not need update
  1040. continue;
  1041. }
  1042. $thisprogress = $progresstotal;
  1043. foreach ($grade_items as $item) {
  1044. if ($item->needsupdate) {
  1045. $thisprogress--;
  1046. }
  1047. }
  1048. // Clip between $progresscurrent and $progresstotal.
  1049. $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
  1050. $progress->progress($thisprogress);
  1051. $progresscurrent = $thisprogress;
  1052. foreach ($depends_on[$gid] as $did) {
  1053. if (!in_array($did, $finalids)) {
  1054. // This item depends on something that is not yet in finals array.
  1055. continue 2;
  1056. }
  1057. }
  1058. // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
  1059. // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
  1060. // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
  1061. // but any dependant in the cascade) have not been updated.
  1062. // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
  1063. // depend on $updated_item.
  1064. // Here we check to see if the direct decendants are marked as updated.
  1065. if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
  1066. // We need to ensure that none of this item's dependencies have been updated.
  1067. // If we find that one of the direct decendants of this grade item is marked as updated then this
  1068. // grade item needs to be recalculated and marked as updated.
  1069. // Being marked as updated is done further down in the code.
  1070. $updateddependencies = false;
  1071. foreach ($depends_on[$gid] as $dependency) {
  1072. if (in_array($dependency, $updatedids)) {
  1073. $updateddependencies = true;
  1074. break;
  1075. }
  1076. }
  1077. if ($updateddependencies === false) {
  1078. // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
  1079. // as final.
  1080. $count++;
  1081. $finalids[] = $gid;
  1082. continue;
  1083. }
  1084. }
  1085. // Let's update, calculate or aggregate.
  1086. $result = $grade_items[$gid]->regrade_final_grades($userid);
  1087. if ($result === true) {
  1088. // We …

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