PageRenderTime 64ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/grade/tests/grade_category_test.php

https://bitbucket.org/moodle/moodle
PHP | 916 lines | 634 code | 141 blank | 141 comment | 5 complexity | 2cea871593950d071d1c64cd033844f9 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  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. * @package core_grades
  18. * @category phpunit
  19. * @copyright nicolas@moodle.com
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. defined('MOODLE_INTERNAL') || die();
  23. require_once(__DIR__.'/fixtures/lib.php');
  24. class core_grade_category_testcase extends grade_base_testcase {
  25. public function test_grade_category() {
  26. $this->sub_test_grade_category_construct();
  27. $this->sub_test_grade_category_build_path();
  28. $this->sub_test_grade_category_fetch();
  29. $this->sub_test_grade_category_fetch_all();
  30. $this->sub_test_grade_category_update();
  31. $this->sub_test_grade_category_delete();
  32. $this->sub_test_grade_category_insert();
  33. $this->sub_test_grade_category_qualifies_for_regrading();
  34. $this->sub_test_grade_category_force_regrading();
  35. $this->sub_test_grade_category_aggregate_grades();
  36. $this->sub_test_grade_category_apply_limit_rules();
  37. $this->sub_test_grade_category_is_aggregationcoef_used();
  38. $this->sub_test_grade_category_aggregation_uses_aggregationcoef();
  39. $this->sub_test_grade_category_fetch_course_tree();
  40. $this->sub_test_grade_category_get_children();
  41. $this->sub_test_grade_category_load_grade_item();
  42. $this->sub_test_grade_category_get_grade_item();
  43. $this->sub_test_grade_category_load_parent_category();
  44. $this->sub_test_grade_category_get_parent_category();
  45. $this->sub_test_grade_category_get_name_escaped();
  46. $this->sub_test_grade_category_get_name_unescaped();
  47. $this->sub_test_grade_category_generate_grades_aggregationweight();
  48. $this->sub_test_grade_category_set_parent();
  49. $this->sub_test_grade_category_get_final();
  50. $this->sub_test_grade_category_get_sortorder();
  51. $this->sub_test_grade_category_set_sortorder();
  52. $this->sub_test_grade_category_is_editable();
  53. $this->sub_test_grade_category_move_after_sortorder();
  54. $this->sub_test_grade_category_is_course_category();
  55. $this->sub_test_grade_category_fetch_course_category();
  56. $this->sub_test_grade_category_is_locked();
  57. $this->sub_test_grade_category_set_locked();
  58. $this->sub_test_grade_category_is_hidden();
  59. $this->sub_test_grade_category_set_hidden();
  60. $this->sub_test_grade_category_can_control_visibility();
  61. $this->sub_test_grade_category_total_visibility();
  62. // This won't work until MDL-11837 is complete.
  63. // $this->sub_test_grade_category_generate_grades();
  64. // Do this last as adding a second course category messes up the data.
  65. $this->sub_test_grade_category_insert_course_category();
  66. $this->sub_test_grade_category_is_extracredit_used();
  67. $this->sub_test_grade_category_aggregation_uses_extracredit();
  68. }
  69. // Adds 3 new grade categories at various depths.
  70. protected function sub_test_grade_category_construct() {
  71. $course_category = grade_category::fetch_course_category($this->courseid);
  72. $params = new stdClass();
  73. $params->courseid = $this->courseid;
  74. $params->fullname = 'unittestcategory4';
  75. $grade_category = new grade_category($params, false);
  76. $grade_category->insert();
  77. $this->grade_categories[] = $grade_category;
  78. $this->assertEquals($params->courseid, $grade_category->courseid);
  79. $this->assertEquals($params->fullname, $grade_category->fullname);
  80. $this->assertEquals(2, $grade_category->depth);
  81. $this->assertEquals("/$course_category->id/$grade_category->id/", $grade_category->path);
  82. $parentpath = $grade_category->path;
  83. // Test a child category.
  84. $params->parent = $grade_category->id;
  85. $params->fullname = 'unittestcategory5';
  86. $grade_category = new grade_category($params, false);
  87. $grade_category->insert();
  88. $this->grade_categories[] = $grade_category;
  89. $this->assertEquals(3, $grade_category->depth);
  90. $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
  91. $parentpath = $grade_category->path;
  92. // Test a third depth category.
  93. $params->parent = $grade_category->id;
  94. $params->fullname = 'unittestcategory6';
  95. $grade_category = new grade_category($params, false);
  96. $grade_category->insert();
  97. $this->grade_categories[50] = $grade_category;// Going to delete this one later hence the special index.
  98. $this->assertEquals(4, $grade_category->depth);
  99. $this->assertEquals($parentpath.$grade_category->id."/", $grade_category->path);
  100. }
  101. protected function sub_test_grade_category_build_path() {
  102. $grade_category = new grade_category($this->grade_categories[1]);
  103. $this->assertTrue(method_exists($grade_category, 'build_path'));
  104. $path = grade_category::build_path($grade_category);
  105. $this->assertEquals($grade_category->path, $path);
  106. }
  107. protected function sub_test_grade_category_fetch() {
  108. $grade_category = new grade_category();
  109. $this->assertTrue(method_exists($grade_category, 'fetch'));
  110. $grade_category = grade_category::fetch(array('id'=>$this->grade_categories[0]->id));
  111. $this->assertEquals($this->grade_categories[0]->id, $grade_category->id);
  112. $this->assertEquals($this->grade_categories[0]->fullname, $grade_category->fullname);
  113. }
  114. protected function sub_test_grade_category_fetch_all() {
  115. $grade_category = new grade_category();
  116. $this->assertTrue(method_exists($grade_category, 'fetch_all'));
  117. $grade_categories = grade_category::fetch_all(array('courseid'=>$this->courseid));
  118. $this->assertEquals(count($this->grade_categories), count($grade_categories)-1);
  119. }
  120. protected function sub_test_grade_category_update() {
  121. global $DB;
  122. $grade_category = new grade_category($this->grade_categories[0]);
  123. $this->assertTrue(method_exists($grade_category, 'update'));
  124. $grade_category->fullname = 'Updated info for this unittest grade_category';
  125. $grade_category->path = null; // Path must be recalculated if missing.
  126. $grade_category->depth = null;
  127. $grade_category->aggregation = GRADE_AGGREGATE_MAX; // Should force regrading.
  128. $grade_item = $grade_category->get_grade_item();
  129. $this->assertEquals(0, $grade_item->needsupdate);
  130. $this->assertTrue($grade_category->update());
  131. $fullname = $DB->get_field('grade_categories', 'fullname', array('id' => $this->grade_categories[0]->id));
  132. $this->assertEquals($grade_category->fullname, $fullname);
  133. $path = $DB->get_field('grade_categories', 'path', array('id' => $this->grade_categories[0]->id));
  134. $this->assertEquals($grade_category->path, $path);
  135. $depth = $DB->get_field('grade_categories', 'depth', array('id' => $this->grade_categories[0]->id));
  136. $this->assertEquals($grade_category->depth, $depth);
  137. $grade_item = $grade_category->get_grade_item();
  138. $this->assertEquals(1, $grade_item->needsupdate);
  139. }
  140. protected function sub_test_grade_category_delete() {
  141. global $DB;
  142. $grade_category = new grade_category($this->grade_categories[50]);
  143. $this->assertTrue(method_exists($grade_category, 'delete'));
  144. $this->assertTrue($grade_category->delete());
  145. $this->assertFalse($DB->get_record('grade_categories', array('id' => $grade_category->id)));
  146. }
  147. protected function sub_test_grade_category_insert() {
  148. $course_category = grade_category::fetch_course_category($this->courseid);
  149. $grade_category = new grade_category();
  150. $this->assertTrue(method_exists($grade_category, 'insert'));
  151. $grade_category->fullname = 'unittestcategory4';
  152. $grade_category->courseid = $this->courseid;
  153. $grade_category->aggregation = GRADE_AGGREGATE_MEAN;
  154. $grade_category->aggregateonlygraded = 1;
  155. $grade_category->keephigh = 100;
  156. $grade_category->droplow = 10;
  157. $grade_category->hidden = 0;
  158. $grade_category->parent = $this->grade_categories[1]->id; // sub_test_grade_category_delete() removed the category at 0.
  159. $grade_category->insert();
  160. $this->assertEquals('/'.$course_category->id.'/'.$this->grade_categories[1]->parent.'/'.$this->grade_categories[1]->id.'/'.$grade_category->id.'/', $grade_category->path);
  161. $this->assertEquals(4, $grade_category->depth);
  162. $last_grade_category = end($this->grade_categories);
  163. $this->assertFalse(empty($grade_category->grade_item));
  164. $this->assertEquals($grade_category->id, $grade_category->grade_item->iteminstance);
  165. $this->assertEquals('category', $grade_category->grade_item->itemtype);
  166. $this->assertEquals($grade_category->id, $last_grade_category->id + 1);
  167. $this->assertFalse(empty($grade_category->timecreated));
  168. $this->assertFalse(empty($grade_category->timemodified));
  169. }
  170. protected function sub_test_grade_category_qualifies_for_regrading() {
  171. $grade_category = new grade_category($this->grade_categories[1]);
  172. $this->assertTrue(method_exists($grade_category, 'qualifies_for_regrading'));
  173. $this->assertFalse($grade_category->qualifies_for_regrading());
  174. $grade_category->aggregation = GRADE_AGGREGATE_MAX;
  175. $this->assertTrue($grade_category->qualifies_for_regrading());
  176. $grade_category = new grade_category($this->grade_categories[1]);
  177. $grade_category->droplow = 99;
  178. $this->assertTrue($grade_category->qualifies_for_regrading());
  179. $grade_category = new grade_category($this->grade_categories[1]);
  180. $grade_category->keephigh = 99;
  181. $this->assertTrue($grade_category->qualifies_for_regrading());
  182. }
  183. protected function sub_test_grade_category_force_regrading() {
  184. $grade_category = new grade_category($this->grade_categories[1]);
  185. $this->assertTrue(method_exists($grade_category, 'force_regrading'));
  186. $grade_category->load_grade_item();
  187. $this->assertEquals(0, $grade_category->grade_item->needsupdate);
  188. $grade_category->force_regrading();
  189. $grade_category->grade_item = null;
  190. $grade_category->load_grade_item();
  191. $this->assertEquals(1, $grade_category->grade_item->needsupdate);
  192. }
  193. /**
  194. * Tests the setting of the grade_grades aggregationweight column.
  195. * Currently, this is only a regression test for MDL-51715.
  196. * This must be run before sub_test_grade_category_set_parent(), which alters
  197. * the fixture.
  198. */
  199. protected function sub_test_grade_category_generate_grades_aggregationweight() {
  200. global $DB;
  201. // Start of regression test for MDL-51715.
  202. // grade_categories [1] and [2] are child categories of [0]
  203. // Ensure that grades have been generated with fixture data.
  204. $childcat1 = new grade_category($this->grade_categories[1]);
  205. $childcat1itemid = $childcat1->load_grade_item()->id;
  206. $childcat1->generate_grades();
  207. $childcat2 = new grade_category($this->grade_categories[2]);
  208. $childcat2itemid = $childcat2->load_grade_item()->id;
  209. $childcat2->generate_grades();
  210. $parentcat = new grade_category($this->grade_categories[0]);
  211. $parentcat->generate_grades();
  212. // Drop low and and re-generate to produce 'dropped' aggregation status.
  213. $parentcat->droplow = 1;
  214. $parentcat->generate_grades();
  215. $this->assertTrue($DB->record_exists_select(
  216. 'grade_grades',
  217. "aggregationstatus='dropped' and itemid in (?,?)",
  218. array($childcat1itemid, $childcat2itemid)));
  219. $this->assertFalse($DB->record_exists_select(
  220. 'grade_grades',
  221. "aggregationstatus='dropped' and aggregationweight > 0.00"),
  222. "aggregationweight should be 0.00 if aggregationstatus=='dropped'");
  223. // Reset grade data to be consistent with fixture data.
  224. $parentcat->droplow = 0;
  225. $parentcat->generate_grades();
  226. // Blank out the final grade for one of the child categories and re-generate
  227. // to produce 'novalue' aggregationstatus. Direct DB update is testing shortcut.
  228. $DB->set_field('grade_grades', 'finalgrade', null, array('itemid'=>$childcat1itemid));
  229. $parentcat->generate_grades();
  230. $this->assertFalse($DB->record_exists_select(
  231. 'grade_grades',
  232. "aggregationstatus='dropped' and itemid in (?,?)",
  233. array($childcat1itemid, $childcat2itemid)));
  234. $this->assertTrue($DB->record_exists_select(
  235. 'grade_grades',
  236. "aggregationstatus='novalue' and itemid = ?",
  237. array($childcat1itemid)));
  238. $this->assertFalse($DB->record_exists_select(
  239. 'grade_grades',
  240. "aggregationstatus='novalue' and aggregationweight > 0.00"),
  241. "aggregationweight should be 0.00 if aggregationstatus=='novalue'");
  242. // Re-generate to be consistent with fixture data.
  243. $childcat1->generate_grades();
  244. $parentcat->generate_grades();
  245. // End of regression test for MDL-51715.
  246. }
  247. /**
  248. * Tests the calculation of grades using the various aggregation methods with and without hidden grades
  249. * This will not work entirely until MDL-11837 is done
  250. */
  251. protected function sub_test_grade_category_generate_grades() {
  252. global $DB;
  253. // Inserting some special grade items to make testing the final grade calculation easier.
  254. $params = new stdClass();
  255. $params->courseid = $this->courseid;
  256. $params->fullname = 'unittestgradecalccategory';
  257. $params->aggregation = GRADE_AGGREGATE_MEAN;
  258. $params->aggregateonlygraded = 0;
  259. $grade_category = new grade_category($params, false);
  260. $grade_category->insert();
  261. $this->assertTrue(method_exists($grade_category, 'generate_grades'));
  262. $grade_category->load_grade_item();
  263. $cgi = $grade_category->get_grade_item();
  264. $cgi->grademin = 0;
  265. $cgi->grademax = 20; // 3 grade items out of 10 but category is out of 20 to force scaling to occur.
  266. $cgi->update();
  267. // 3 grade items each with a maximum grade of 10.
  268. $grade_items = array();
  269. for ($i=0; $i<3; $i++) {
  270. $grade_items[$i] = new grade_item();
  271. $grade_items[$i]->courseid = $this->courseid;
  272. $grade_items[$i]->categoryid = $grade_category->id;
  273. $grade_items[$i]->itemname = 'manual grade_item '.$i;
  274. $grade_items[$i]->itemtype = 'manual';
  275. $grade_items[$i]->itemnumber = 0;
  276. $grade_items[$i]->needsupdate = false;
  277. $grade_items[$i]->gradetype = GRADE_TYPE_VALUE;
  278. $grade_items[$i]->grademin = 0;
  279. $grade_items[$i]->grademax = 10;
  280. $grade_items[$i]->iteminfo = 'Manual grade item used for unit testing';
  281. $grade_items[$i]->timecreated = time();
  282. $grade_items[$i]->timemodified = time();
  283. // Used as the weight by weighted mean and as extra credit by mean with extra credit.
  284. // Will be 0, 1 and 2.
  285. $grade_items[$i]->aggregationcoef = $i;
  286. $grade_items[$i]->insert();
  287. }
  288. // A grade for each grade item.
  289. $grade_grades = array();
  290. for ($i=0; $i<3; $i++) {
  291. $grade_grades[$i] = new grade_grade();
  292. $grade_grades[$i]->itemid = $grade_items[$i]->id;
  293. $grade_grades[$i]->userid = $this->userid;
  294. $grade_grades[$i]->rawgrade = ($i+1)*2; // Produce grade grades of 2, 4 and 6.
  295. $grade_grades[$i]->finalgrade = ($i+1)*2;
  296. $grade_grades[$i]->timecreated = time();
  297. $grade_grades[$i]->timemodified = time();
  298. $grade_grades[$i]->information = '1 of 2 grade_grades';
  299. $grade_grades[$i]->informationformat = FORMAT_PLAIN;
  300. $grade_grades[$i]->feedback = 'Good, but not good enough..';
  301. $grade_grades[$i]->feedbackformat = FORMAT_PLAIN;
  302. $grade_grades[$i]->insert();
  303. }
  304. // 3 grade items with 1 grade_grade each.
  305. // grade grades have the values 2, 4 and 6.
  306. // First correct answer is the aggregate with all 3 grades.
  307. // Second correct answer is with the first grade (value 2) hidden.
  308. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEDIAN, 'GRADE_AGGREGATE_MEDIAN', 8, 8);
  309. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MAX, 'GRADE_AGGREGATE_MAX', 12, 12);
  310. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MODE, 'GRADE_AGGREGATE_MODE', 12, 12);
  311. // Weighted mean. note grade totals are rounded to an int to prevent rounding discrepancies. correct final grade isnt actually exactly 10
  312. // 3 items with grades 2, 4 and 6 with weights 0, 1 and 2 and all out of 10. then doubled to be out of 20.
  313. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN, 'GRADE_AGGREGATE_WEIGHTED_MEAN', 10, 10);
  314. // Simple weighted mean.
  315. // 3 items with grades 2, 4 and 6 equally weighted and all out of 10. then doubled to be out of 20.
  316. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_WEIGHTED_MEAN2, 'GRADE_AGGREGATE_WEIGHTED_MEAN2', 8, 10);
  317. // Mean of grades with extra credit.
  318. // 3 items with grades 2, 4 and 6 with extra credit 0, 1 and 2 equally weighted and all out of 10. then doubled to be out of 20.
  319. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_EXTRACREDIT_MEAN, 'GRADE_AGGREGATE_EXTRACREDIT_MEAN', 10, 13);
  320. // Aggregation tests the are affected by a hidden grade currently dont work as we dont store the altered grade in the database
  321. // instead an in memory recalculation is done. This should be remedied by MDL-11837.
  322. // Fails with 1 grade hidden. still reports 8 as being correct.
  323. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MEAN, 'GRADE_AGGREGATE_MEAN', 8, 10);
  324. // Fails with 1 grade hidden. still reports 4 as being correct.
  325. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_MIN, 'GRADE_AGGREGATE_MIN', 4, 8);
  326. // Fails with 1 grade hidden. still reports 12 as being correct.
  327. $this->helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, GRADE_AGGREGATE_SUM, 'GRADE_AGGREGATE_SUM', 12, 10);
  328. }
  329. /**
  330. * Test grade category aggregation using the supplied grade objects and aggregation method
  331. * @param grade_category $grade_category the category to be tested
  332. * @param array $grade_items array of instance of grade_item
  333. * @param array $grade_grades array of instances of grade_grade
  334. * @param int $aggmethod the aggregation method to apply ie GRADE_AGGREGATE_MEAN
  335. * @param string $aggmethodname the name of the aggregation method to apply. Used to display any test failure messages
  336. * @param int $correct1 the correct final grade for the category with NO items hidden
  337. * @param int $correct2 the correct final grade for the category with the grade at $grade_grades[0] hidden
  338. * @return void
  339. */
  340. protected function helper_test_grade_agg_method($grade_category, $grade_items, $grade_grades, $aggmethod, $aggmethodname, $correct1, $correct2) {
  341. $grade_category->aggregation = $aggmethod;
  342. $grade_category->update();
  343. // Check grade_item isnt hidden from a previous test.
  344. $grade_items[0]->set_hidden(0, true);
  345. $this->helper_test_grade_aggregation_result($grade_category, $correct1, 'Testing aggregation method('.$aggmethodname.') with no items hidden %s');
  346. // Hide the grade item with grade of 2.
  347. $grade_items[0]->set_hidden(1, true);
  348. $this->helper_test_grade_aggregation_result($grade_category, $correct2, 'Testing aggregation method('.$aggmethodname.') with 1 item hidden %s');
  349. }
  350. /**
  351. * Verify the value of the category grade item for $this->userid
  352. * @param grade_category $grade_category the category to be tested
  353. * @param int $correctgrade the expected grade
  354. * @param string msg The message that should be displayed if the correct grade is not found
  355. * @return void
  356. */
  357. protected function helper_test_grade_aggregation_result($grade_category, $correctgrade, $msg) {
  358. global $DB;
  359. $category_grade_item = $grade_category->get_grade_item();
  360. // This creates all the grade_grades we need.
  361. grade_regrade_final_grades($this->courseid);
  362. $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
  363. $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  364. $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
  365. /*
  366. * TODO this doesnt work as the grade_grades created by $grade_category->generate_grades(); dont
  367. * observe the category's max grade
  368. // delete the grade_grades for the category itself and check they get recreated correctly.
  369. $DB->delete_records('grade_grades', array('itemid'=>$category_grade_item->id));
  370. $grade_category->generate_grades();
  371. $grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
  372. $this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
  373. $this->assertEquals(intval($correctgrade), intval($grade->finalgrade), $msg);
  374. *
  375. */
  376. }
  377. protected function sub_test_grade_category_aggregate_grades() {
  378. $category = new grade_category($this->grade_categories[0]);
  379. $this->assertTrue(method_exists($category, 'aggregate_grades'));
  380. // Tested more fully via test_grade_category_generate_grades().
  381. }
  382. protected function sub_test_grade_category_apply_limit_rules() {
  383. $items[$this->grade_items[0]->id] = new grade_item($this->grade_items[0], false);
  384. $items[$this->grade_items[1]->id] = new grade_item($this->grade_items[1], false);
  385. $items[$this->grade_items[2]->id] = new grade_item($this->grade_items[2], false);
  386. $items[$this->grade_items[4]->id] = new grade_item($this->grade_items[4], false);
  387. // Test excluding the lowest 2 out of 4 grades from aggregation with no 0 grades.
  388. $category = new grade_category();
  389. $category->droplow = 2;
  390. $grades = array($this->grade_items[0]->id=>5.374,
  391. $this->grade_items[1]->id=>9.4743,
  392. $this->grade_items[2]->id=>2.5474,
  393. $this->grade_items[4]->id=>7.3754);
  394. $category->apply_limit_rules($grades, $items);
  395. $this->assertEquals(count($grades), 2);
  396. $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
  397. $this->assertEquals($grades[$this->grade_items[4]->id], 7.3754);
  398. // Test aggregating only the highest 1 out of 4 grades.
  399. $category = new grade_category();
  400. $category->keephigh = 1;
  401. $category->droplow = 0;
  402. $grades = array($this->grade_items[0]->id=>5.374,
  403. $this->grade_items[1]->id=>9.4743,
  404. $this->grade_items[2]->id=>2.5474,
  405. $this->grade_items[4]->id=>7.3754);
  406. $category->apply_limit_rules($grades, $items);
  407. $this->assertEquals(count($grades), 1);
  408. $grade = reset($grades);
  409. $this->assertEquals(9.4743, $grade);
  410. // Test excluding the lowest 2 out of 4 grades from aggregation with no 0 grades.
  411. // An extra credit grade item should be kept even if droplow means it would otherwise be excluded.
  412. $category = new grade_category();
  413. $category->droplow = 2;
  414. $category->aggregation = GRADE_AGGREGATE_SUM;
  415. $items[$this->grade_items[2]->id]->aggregationcoef = 1; // Mark grade item 2 as "extra credit".
  416. $grades = array($this->grade_items[0]->id=>5.374,
  417. $this->grade_items[1]->id=>9.4743,
  418. $this->grade_items[2]->id=>2.5474,
  419. $this->grade_items[4]->id=>7.3754);
  420. $category->apply_limit_rules($grades, $items);
  421. $this->assertEquals(count($grades), 2);
  422. $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
  423. $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
  424. // Test only aggregating the highest 1 out of 4 grades.
  425. // An extra credit grade item is retained in addition to the highest grade.
  426. $category = new grade_category();
  427. $category->keephigh = 1;
  428. $category->droplow = 0;
  429. $category->aggregation = GRADE_AGGREGATE_SUM;
  430. $items[$this->grade_items[2]->id]->aggregationcoef = 1; // Mark grade item 2 as "extra credit".
  431. $grades = array($this->grade_items[0]->id=>5.374,
  432. $this->grade_items[1]->id=>9.4743,
  433. $this->grade_items[2]->id=>2.5474,
  434. $this->grade_items[4]->id=>7.3754);
  435. $category->apply_limit_rules($grades, $items);
  436. $this->assertEquals(count($grades), 2);
  437. $this->assertEquals($grades[$this->grade_items[1]->id], 9.4743);
  438. $this->assertEquals($grades[$this->grade_items[2]->id], 2.5474);
  439. // Test excluding the lowest 1 out of 4 grades from aggregation with two 0 grades.
  440. $items[$this->grade_items[2]->id]->aggregationcoef = 0; // Undo marking grade item 2 as "extra credit".
  441. $category = new grade_category();
  442. $category->droplow = 1;
  443. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
  444. $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
  445. $this->grade_items[1]->id=>5, // 5 out of 100.
  446. $this->grade_items[2]->id=>2, // 0 out of 6.
  447. $this->grade_items[4]->id=>0); // 0 out of 100.
  448. $category->apply_limit_rules($grades, $items);
  449. $this->assertEquals(count($grades), 3);
  450. $this->assertEquals($grades[$this->grade_items[1]->id], 5);
  451. $this->assertEquals($grades[$this->grade_items[2]->id], 2);
  452. $this->assertEquals($grades[$this->grade_items[4]->id], 0);
  453. // Test excluding the lowest 2 out of 4 grades from aggregation with three 0 grades.
  454. $category = new grade_category();
  455. $category->droplow = 2;
  456. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
  457. $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
  458. $this->grade_items[1]->id=>5, // 5 out of 100.
  459. $this->grade_items[2]->id=>0, // 0 out of 6.
  460. $this->grade_items[4]->id=>0); // 0 out of 100. Should be excluded from aggregation.
  461. $category->apply_limit_rules($grades, $items);
  462. $this->assertEquals(count($grades), 2);
  463. $this->assertEquals($grades[$this->grade_items[1]->id], 5);
  464. $this->assertEquals($grades[$this->grade_items[2]->id], 0);
  465. // Test excluding the lowest 5 out of 4 grades from aggregation.
  466. // Just to check we handle this sensibly.
  467. $category = new grade_category();
  468. $category->droplow = 5;
  469. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
  470. $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
  471. $this->grade_items[1]->id=>5, // 5 out of 100.
  472. $this->grade_items[2]->id=>6, // 6 out of 6.
  473. $this->grade_items[4]->id=>1);// 1 out of 100. Should be excluded from aggregation.
  474. $category->apply_limit_rules($grades, $items);
  475. $this->assertEquals(count($grades), 0);
  476. // Test excluding the lowest 4 out of 4 grades from aggregation with one marked as extra credit.
  477. $category = new grade_category();
  478. $category->droplow = 4;
  479. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
  480. $items[$this->grade_items[2]->id]->aggregationcoef = 1; // Mark grade item 2 as "extra credit".
  481. $grades = array($this->grade_items[0]->id=>0, // 0 out of 110. Should be excluded from aggregation.
  482. $this->grade_items[1]->id=>5, // 5 out of 100. Should be excluded from aggregation.
  483. $this->grade_items[2]->id=>6, // 6 out of 6. Extra credit. Should be retained.
  484. $this->grade_items[4]->id=>1);// 1 out of 100. Should be excluded from aggregation.
  485. $category->apply_limit_rules($grades, $items);
  486. $this->assertEquals(count($grades), 1);
  487. $this->assertEquals($grades[$this->grade_items[2]->id], 6);
  488. // MDL-35667 - There was an infinite loop if several items had the same grade and at least one was extra credit.
  489. $category = new grade_category();
  490. $category->droplow = 1;
  491. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // Simple weighted mean.
  492. $items[$this->grade_items[1]->id]->aggregationcoef = 1; // Mark grade item 1 as "extra credit".
  493. $grades = array($this->grade_items[0]->id=>1, // 1 out of 110. Should be excluded from aggregation.
  494. $this->grade_items[1]->id=>1, // 1 out of 100. Extra credit. Should be retained.
  495. $this->grade_items[2]->id=>1, // 1 out of 6. Should be retained.
  496. $this->grade_items[4]->id=>1);// 1 out of 100. Should be retained.
  497. $category->apply_limit_rules($grades, $items);
  498. $this->assertEquals(count($grades), 3);
  499. $this->assertEquals($grades[$this->grade_items[1]->id], 1);
  500. $this->assertEquals($grades[$this->grade_items[2]->id], 1);
  501. $this->assertEquals($grades[$this->grade_items[4]->id], 1);
  502. }
  503. protected function sub_test_grade_category_is_aggregationcoef_used() {
  504. $category = new grade_category();
  505. // Following use aggregationcoef.
  506. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN;
  507. $this->assertTrue($category->is_aggregationcoef_used());
  508. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
  509. $this->assertTrue($category->is_aggregationcoef_used());
  510. $category->aggregation = GRADE_AGGREGATE_EXTRACREDIT_MEAN;
  511. $this->assertTrue($category->is_aggregationcoef_used());
  512. $category->aggregation = GRADE_AGGREGATE_SUM;
  513. $this->assertTrue($category->is_aggregationcoef_used());
  514. // Following don't use aggregationcoef.
  515. $category->aggregation = GRADE_AGGREGATE_MAX;
  516. $this->assertFalse($category->is_aggregationcoef_used());
  517. $category->aggregation = GRADE_AGGREGATE_MEAN;
  518. $this->assertFalse($category->is_aggregationcoef_used());
  519. $category->aggregation = GRADE_AGGREGATE_MEDIAN;
  520. $this->assertFalse($category->is_aggregationcoef_used());
  521. $category->aggregation = GRADE_AGGREGATE_MIN;
  522. $this->assertFalse($category->is_aggregationcoef_used());
  523. $category->aggregation = GRADE_AGGREGATE_MODE;
  524. $this->assertFalse($category->is_aggregationcoef_used());
  525. }
  526. protected function sub_test_grade_category_aggregation_uses_aggregationcoef() {
  527. $this->assertTrue(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_WEIGHTED_MEAN));
  528. $this->assertTrue(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_WEIGHTED_MEAN2));
  529. $this->assertTrue(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_EXTRACREDIT_MEAN));
  530. $this->assertTrue(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_SUM));
  531. $this->assertFalse(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MAX));
  532. $this->assertFalse(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MEAN));
  533. $this->assertFalse(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MEDIAN));
  534. $this->assertFalse(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MIN));
  535. $this->assertFalse(grade_category::aggregation_uses_aggregationcoef(GRADE_AGGREGATE_MODE));
  536. }
  537. protected function sub_test_grade_category_fetch_course_tree() {
  538. $category = new grade_category();
  539. $this->assertTrue(method_exists($category, 'fetch_course_tree'));
  540. // TODO: add some tests.
  541. }
  542. protected function sub_test_grade_category_get_children() {
  543. $course_category = grade_category::fetch_course_category($this->courseid);
  544. $category = new grade_category($this->grade_categories[0]);
  545. $this->assertTrue(method_exists($category, 'get_children'));
  546. $children_array = $category->get_children(0);
  547. $this->assertTrue(is_array($children_array));
  548. $this->assertFalse(empty($children_array[2]));
  549. $this->assertFalse(empty($children_array[2]['object']));
  550. $this->assertFalse(empty($children_array[2]['children']));
  551. $this->assertEquals($this->grade_categories[1]->id, $children_array[2]['object']->id);
  552. $this->assertEquals($this->grade_categories[2]->id, $children_array[5]['object']->id);
  553. $this->assertEquals($this->grade_items[0]->id, $children_array[2]['children'][3]['object']->id);
  554. $this->assertEquals($this->grade_items[1]->id, $children_array[2]['children'][4]['object']->id);
  555. $this->assertEquals($this->grade_items[2]->id, $children_array[5]['children'][6]['object']->id);
  556. }
  557. protected function sub_test_grade_category_load_grade_item() {
  558. $category = new grade_category($this->grade_categories[0]);
  559. $this->assertTrue(method_exists($category, 'load_grade_item'));
  560. $this->assertEquals(null, $category->grade_item);
  561. $category->load_grade_item();
  562. $this->assertEquals($this->grade_items[3]->id, $category->grade_item->id);
  563. }
  564. protected function sub_test_grade_category_get_grade_item() {
  565. $category = new grade_category($this->grade_categories[0]);
  566. $this->assertTrue(method_exists($category, 'get_grade_item'));
  567. $grade_item = $category->get_grade_item();
  568. $this->assertEquals($this->grade_items[3]->id, $grade_item->id);
  569. }
  570. protected function sub_test_grade_category_load_parent_category() {
  571. $category = new grade_category($this->grade_categories[1]);
  572. $this->assertTrue(method_exists($category, 'load_parent_category'));
  573. $this->assertEquals(null, $category->parent_category);
  574. $category->load_parent_category();
  575. $this->assertEquals($this->grade_categories[0]->id, $category->parent_category->id);
  576. }
  577. protected function sub_test_grade_category_get_parent_category() {
  578. $category = new grade_category($this->grade_categories[1]);
  579. $this->assertTrue(method_exists($category, 'get_parent_category'));
  580. $parent_category = $category->get_parent_category();
  581. $this->assertEquals($this->grade_categories[0]->id, $parent_category->id);
  582. }
  583. /**
  584. * Tests the getter of the category fullname with escaped HTML.
  585. */
  586. protected function sub_test_grade_category_get_name_escaped() {
  587. $category = new grade_category($this->grade_categories[0]);
  588. $this->assertTrue(method_exists($category, 'get_name'));
  589. $this->assertEquals(format_string($this->grade_categories[0]->fullname, true, ['escape' => true]),
  590. $category->get_name(true));
  591. }
  592. /**
  593. * Tests the getter of the category fullname with unescaped HTML.
  594. */
  595. protected function sub_test_grade_category_get_name_unescaped() {
  596. $category = new grade_category($this->grade_categories[0]);
  597. $this->assertTrue(method_exists($category, 'get_name'));
  598. $this->assertEquals(format_string($this->grade_categories[0]->fullname, true, ['escape' => false]),
  599. $category->get_name(false));
  600. }
  601. protected function sub_test_grade_category_set_parent() {
  602. $category = new grade_category($this->grade_categories[1]);
  603. $this->assertTrue(method_exists($category, 'set_parent'));
  604. // TODO: implement detailed tests.
  605. $course_category = grade_category::fetch_course_category($this->courseid);
  606. $this->assertTrue($category->set_parent($course_category->id));
  607. $this->assertEquals($course_category->id, $category->parent);
  608. }
  609. protected function sub_test_grade_category_get_final() {
  610. $category = new grade_category($this->grade_categories[0]);
  611. $this->assertTrue(method_exists($category, 'get_final'));
  612. $category->load_grade_item();
  613. $this->assertEquals($category->get_final(), $category->grade_item->get_final());
  614. }
  615. protected function sub_test_grade_category_get_sortorder() {
  616. $category = new grade_category($this->grade_categories[0]);
  617. $this->assertTrue(method_exists($category, 'get_sortorder'));
  618. $category->load_grade_item();
  619. $this->assertEquals($category->get_sortorder(), $category->grade_item->get_sortorder());
  620. }
  621. protected function sub_test_grade_category_set_sortorder() {
  622. $category = new grade_category($this->grade_categories[0]);
  623. $this->assertTrue(method_exists($category, 'set_sortorder'));
  624. $category->load_grade_item();
  625. $this->assertEquals($category->set_sortorder(10), $category->grade_item->set_sortorder(10));
  626. }
  627. protected function sub_test_grade_category_move_after_sortorder() {
  628. $category = new grade_category($this->grade_categories[0]);
  629. $this->assertTrue(method_exists($category, 'move_after_sortorder'));
  630. $category->load_grade_item();
  631. $this->assertEquals($category->move_after_sortorder(10), $category->grade_item->move_after_sortorder(10));
  632. }
  633. protected function sub_test_grade_category_is_course_category() {
  634. $category = grade_category::fetch_course_category($this->courseid);
  635. $this->assertTrue(method_exists($category, 'is_course_category'));
  636. $this->assertTrue($category->is_course_category());
  637. }
  638. protected function sub_test_grade_category_fetch_course_category() {
  639. $category = new grade_category();
  640. $this->assertTrue(method_exists($category, 'fetch_course_category'));
  641. $category = grade_category::fetch_course_category($this->courseid);
  642. $this->assertTrue(empty($category->parent));
  643. }
  644. /**
  645. * TODO implement
  646. */
  647. protected function sub_test_grade_category_is_editable() {
  648. }
  649. protected function sub_test_grade_category_is_locked() {
  650. $category = new grade_category($this->grade_categories[0]);
  651. $this->assertTrue(method_exists($category, 'is_locked'));
  652. $category->load_grade_item();
  653. $this->assertEquals($category->is_locked(), $category->grade_item->is_locked());
  654. }
  655. protected function sub_test_grade_category_set_locked() {
  656. $category = new grade_category($this->grade_categories[0]);
  657. $this->assertTrue(method_exists($category, 'set_locked'));
  658. // Will return false as cannot lock a grade that needs updating.
  659. $this->assertFalse($category->set_locked(1));
  660. grade_regrade_final_grades($this->courseid);
  661. // Get the category from the db again.
  662. $category = new grade_category($this->grade_categories[0]);
  663. $this->assertTrue($category->set_locked(1));
  664. }
  665. protected function sub_test_grade_category_is_hidden() {
  666. $category = new grade_category($this->grade_categories[0]);
  667. $this->assertTrue(method_exists($category, 'is_hidden'));
  668. $category->load_grade_item();
  669. $this->assertEquals($category->is_hidden(), $category->grade_item->is_hidden());
  670. }
  671. protected function sub_test_grade_category_set_hidden() {
  672. $category = new grade_category($this->grade_categories[0]);
  673. $this->assertTrue(method_exists($category, 'set_hidden'));
  674. $category->set_hidden(1, true);
  675. $category->load_grade_item();
  676. $this->assertEquals(true, $category->grade_item->is_hidden());
  677. }
  678. protected function sub_test_grade_category_can_control_visibility() {
  679. $category = new grade_category($this->grade_categories[0]);
  680. $this->assertTrue($category->can_control_visibility());
  681. }
  682. protected function sub_test_grade_category_insert_course_category() {
  683. // Beware: adding a duplicate course category messes up the data in a way that's hard to recover from.
  684. $grade_category = new grade_category();
  685. $this->assertTrue(method_exists($grade_category, 'insert_course_category'));
  686. $id = $grade_category->insert_course_category($this->courseid);
  687. $this->assertNotNull($id);
  688. $this->assertEquals('?', $grade_category->fullname);
  689. $this->assertEquals(GRADE_AGGREGATE_WEIGHTED_MEAN2, $grade_category->aggregation);
  690. $this->assertEquals("/$id/", $grade_category->path);
  691. $this->assertEquals(1, $grade_category->depth);
  692. $this->assertNull($grade_category->parent);
  693. }
  694. protected function generate_random_raw_grade($item, $userid) {
  695. $grade = new grade_grade();
  696. $grade->itemid = $item->id;
  697. $grade->userid = $userid;
  698. $grade->grademin = 0;
  699. $grade->grademax = 1;
  700. $valuetype = "grade$item->gradetype";
  701. $grade->rawgrade = rand(0, 1000) / 1000;
  702. $grade->insert();
  703. return $grade->rawgrade;
  704. }
  705. protected function sub_test_grade_category_is_extracredit_used() {
  706. $category = new grade_category();
  707. // Following use aggregationcoef.
  708. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
  709. $this->assertTrue($category->is_extracredit_used());
  710. $category->aggregation = GRADE_AGGREGATE_EXTRACREDIT_MEAN;
  711. $this->assertTrue($category->is_extracredit_used());
  712. $category->aggregation = GRADE_AGGREGATE_SUM;
  713. $this->assertTrue($category->is_extracredit_used());
  714. // Following don't use aggregationcoef.
  715. $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN;
  716. $this->assertFalse($category->is_extracredit_used());
  717. $category->aggregation = GRADE_AGGREGATE_MAX;
  718. $this->assertFalse($category->is_extracredit_used());
  719. $category->aggregation = GRADE_AGGREGATE_MEAN;
  720. $this->assertFalse($category->is_extracredit_used());
  721. $category->aggregation = GRADE_AGGREGATE_MEDIAN;
  722. $this->assertFalse($category->is_extracredit_used());
  723. $category->aggregation = GRADE_AGGREGATE_MIN;
  724. $this->assertFalse($category->is_extracredit_used());
  725. $category->aggregation = GRADE_AGGREGATE_MODE;
  726. $this->assertFalse($category->is_extracredit_used());
  727. }
  728. protected function sub_test_grade_category_aggregation_uses_extracredit() {
  729. $this->assertTrue(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_WEIGHTED_MEAN2));
  730. $this->assertTrue(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_EXTRACREDIT_MEAN));
  731. $this->assertTrue(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_SUM));
  732. $this->assertFalse(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_WEIGHTED_MEAN));
  733. $this->assertFalse(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MAX));
  734. $this->assertFalse(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MEAN));
  735. $this->assertFalse(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MEDIAN));
  736. $this->assertFalse(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MIN));
  737. $this->assertFalse(grade_category::aggregation_uses_extracredit(GRADE_AGGREGATE_MODE));
  738. }
  739. /**
  740. * Test for category total visibility.
  741. */
  742. protected function sub_test_grade_category_total_visibility() {
  743. // 15 is a manual grade item in grade_categories[5].
  744. $category = new grade_category($this->grade_categories[5], true);
  745. $gradeitem = new grade_item($this->grade_items[15], true);
  746. // Hide grade category.
  747. $category->set_hidden(true, true);
  748. $this->assertTrue($category->is_hidden());
  749. // Category total is hidden.
  750. $categorytotal = $category->get_grade_item();
  751. $this->assertTrue($categorytotal->is_hidden());
  752. // Manual grade is hidden.
  753. $gradeitem->update_from_db();
  754. $this->assertTrue($gradeitem->is_hidden());
  755. // Unhide manual grade item.
  756. $gradeitem->set_hidden(false);
  757. $this->assertFalse($gradeitem->is_hidden());
  758. // Category is unhidden.
  759. $category->update_from_db();
  760. $this->assertFalse($category->is_hidden());
  761. // Category total remain hidden.
  762. $categorytotal = $category->get_grade_item();
  763. $this->assertTrue($categorytotal->is_hidden());
  764. // Edit manual grade item.
  765. $this->assertFalse($gradeitem->is_locked());
  766. $gradeitem->set_locked(true);
  767. $gradeitem->update_from_db();
  768. $this->assertTrue($gradeitem->is_locked());
  769. // Category total should still be hidden.
  770. $category->update_from_db();
  771. $categorytotal = $category->get_grade_item();
  772. $this->assertTrue($categorytotal->is_hidden());
  773. }
  774. }