forked from moodle/moodle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgrade_item.php
2472 lines (2106 loc) · 87 KB
/
grade_item.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Definition of a class to represent a grade item
*
* @package core_grades
* @category grade
* @copyright 2006 Nicolas Connault
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once('grade_object.php');
/**
* Class representing a grade item.
*
* It is responsible for handling its DB representation, modifying and returning its metadata.
*
* @package core_grades
* @category grade
* @copyright 2006 Nicolas Connault
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grade_item extends grade_object {
/**
* DB Table (used by grade_object).
* @var string $table
*/
public $table = 'grade_items';
/**
* Array of required table fields, must start with 'id'.
* @var array $required_fields
*/
public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
/**
* The course this grade_item belongs to.
* @var int $courseid
*/
public $courseid;
/**
* The category this grade_item belongs to (optional).
* @var int $categoryid
*/
public $categoryid;
/**
* The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
* @var grade_category $item_category
*/
public $item_category;
/**
* The grade_category object referenced by $this->categoryid.
* @var grade_category $parent_category
*/
public $parent_category;
/**
* The name of this grade_item (pushed by the module).
* @var string $itemname
*/
public $itemname;
/**
* e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
* @var string $itemtype
*/
public $itemtype;
/**
* The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
* @var string $itemmodule
*/
public $itemmodule;
/**
* ID of the item module
* @var int $iteminstance
*/
public $iteminstance;
/**
* Number of the item in a series of multiple grades pushed by an activity.
* @var int $itemnumber
*/
public $itemnumber;
/**
* Info and notes about this item.
* @var string $iteminfo
*/
public $iteminfo;
/**
* Arbitrary idnumber provided by the module responsible.
* @var string $idnumber
*/
public $idnumber;
/**
* Calculation string used for this item.
* @var string $calculation
*/
public $calculation;
/**
* Indicates if we already tried to normalize the grade calculation formula.
* This flag helps to minimize db access when broken formulas used in calculation.
* @var bool
*/
public $calculation_normalized;
/**
* Math evaluation object
* @var calc_formula A formula object
*/
public $formula;
/**
* The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
* @var int $gradetype
*/
public $gradetype = GRADE_TYPE_VALUE;
/**
* Maximum allowable grade.
* @var float $grademax
*/
public $grademax = 100;
/**
* Minimum allowable grade.
* @var float $grademin
*/
public $grademin = 0;
/**
* id of the scale, if this grade is based on a scale.
* @var int $scaleid
*/
public $scaleid;
/**
* The grade_scale object referenced by $this->scaleid.
* @var grade_scale $scale
*/
public $scale;
/**
* The id of the optional grade_outcome associated with this grade_item.
* @var int $outcomeid
*/
public $outcomeid;
/**
* The grade_outcome this grade is associated with, if applicable.
* @var grade_outcome $outcome
*/
public $outcome;
/**
* grade required to pass. (grademin <= gradepass <= grademax)
* @var float $gradepass
*/
public $gradepass = 0;
/**
* Multiply all grades by this number.
* @var float $multfactor
*/
public $multfactor = 1.0;
/**
* Add this to all grades.
* @var float $plusfactor
*/
public $plusfactor = 0;
/**
* Aggregation coeficient used for weighted averages or extra credit
* @var float $aggregationcoef
*/
public $aggregationcoef = 0;
/**
* Aggregation coeficient used for weighted averages only
* @var float $aggregationcoef2
*/
public $aggregationcoef2 = 0;
/**
* Sorting order of the columns.
* @var int $sortorder
*/
public $sortorder = 0;
/**
* Display type of the grades (Real, Percentage, Letter, or default).
* @var int $display
*/
public $display = GRADE_DISPLAY_TYPE_DEFAULT;
/**
* The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
* @var int $decimals
*/
public $decimals = null;
/**
* Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
* @var int $locked
*/
public $locked = 0;
/**
* Date after which the grade will be locked. Empty means no automatic locking.
* @var int $locktime
*/
public $locktime = 0;
/**
* If set, the whole column will be recalculated, then this flag will be switched off.
* @var bool $needsupdate
*/
public $needsupdate = 1;
/**
* If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
*/
public $weightoverride = 0;
/**
* Cached dependson array
* @var array An array of cached grade item dependencies.
*/
public $dependson_cache = null;
/**
* Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
*
* @param array $params An array with required parameters for this grade object.
* @param bool $fetch Whether to fetch corresponding row from the database or not,
* optional fields might not be defined if false used
*/
public function __construct($params = null, $fetch = true) {
global $CFG;
// Set grademax from $CFG->gradepointdefault .
self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
parent::__construct($params, $fetch);
}
/**
* In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
* Force regrading if necessary, rounds the float numbers using php function,
* the reason is we need to compare the db value with computed number to skip regrading if possible.
*
* @param string $source from where was the object inserted (mod/forum, manual, etc.)
* @return bool success
*/
public function update($source=null) {
// reset caches
$this->dependson_cache = null;
// Retrieve scale and infer grademax/min from it if needed
$this->load_scale();
// make sure there is not 0 in outcomeid
if (empty($this->outcomeid)) {
$this->outcomeid = null;
}
if ($this->qualifies_for_regrading()) {
$this->force_regrading();
}
$this->timemodified = time();
$this->grademin = grade_floatval($this->grademin);
$this->grademax = grade_floatval($this->grademax);
$this->multfactor = grade_floatval($this->multfactor);
$this->plusfactor = grade_floatval($this->plusfactor);
$this->aggregationcoef = grade_floatval($this->aggregationcoef);
$this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
return parent::update($source);
}
/**
* Compares the values held by this object with those of the matching record in DB, and returns
* whether or not these differences are sufficient to justify an update of all parent objects.
* This assumes that this object has an id number and a matching record in DB. If not, it will return false.
*
* @return bool
*/
public function qualifies_for_regrading() {
if (empty($this->id)) {
return false;
}
$db_item = new grade_item(array('id' => $this->id));
$calculationdiff = $db_item->calculation != $this->calculation;
$categorydiff = $db_item->categoryid != $this->categoryid;
$gradetypediff = $db_item->gradetype != $this->gradetype;
$scaleiddiff = $db_item->scaleid != $this->scaleid;
$outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
$locktimediff = $db_item->locktime != $this->locktime;
$grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
$grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
$multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
$plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
$acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
$acoefdiff2 = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
$weightoverride = grade_floats_different($db_item->weightoverride, $this->weightoverride);
$needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
$lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
|| $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
|| $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
}
/**
* Finds and returns a grade_item instance based on params.
*
* @static
* @param array $params associative arrays varname=>value
* @return grade_item|bool Returns a grade_item instance or false if none found
*/
public static function fetch($params) {
return grade_object::fetch_helper('grade_items', 'grade_item', $params);
}
/**
* Check to see if there are any existing grades for this grade_item.
*
* @return boolean - true if there are valid grades for this grade_item.
*/
public function has_grades() {
global $DB;
$count = $DB->count_records_select('grade_grades',
'itemid = :gradeitemid AND finalgrade IS NOT NULL',
array('gradeitemid' => $this->id));
return $count > 0;
}
/**
* Check to see if there are existing overridden grades for this grade_item.
*
* @return boolean - true if there are overridden grades for this grade_item.
*/
public function has_overridden_grades() {
global $DB;
$count = $DB->count_records_select('grade_grades',
'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
array('gradeitemid' => $this->id));
return $count > 0;
}
/**
* Finds and returns all grade_item instances based on params.
*
* @static
* @param array $params associative arrays varname=>value
* @return array array of grade_item instances or false if none found.
*/
public static function fetch_all($params) {
return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
}
/**
* Delete all grades and force_regrading of parent category.
*
* @param string $source from where was the object deleted (mod/forum, manual, etc.)
* @return bool success
*/
public function delete($source=null) {
$this->delete_all_grades($source);
return parent::delete($source);
}
/**
* Delete all grades
*
* @param string $source from where was the object deleted (mod/forum, manual, etc.)
* @return bool
*/
public function delete_all_grades($source=null) {
if (!$this->is_course_item()) {
$this->force_regrading();
}
if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
foreach ($grades as $grade) {
$grade->delete($source);
}
}
return true;
}
/**
* In addition to perform parent::insert(), calls force_regrading() method too.
*
* @param string $source From where was the object inserted (mod/forum, manual, etc.)
* @return int PK ID if successful, false otherwise
*/
public function insert($source=null) {
global $CFG, $DB;
if (empty($this->courseid)) {
print_error('cannotinsertgrade');
}
// load scale if needed
$this->load_scale();
// add parent category if needed
if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
$course_category = grade_category::fetch_course_category($this->courseid);
$this->categoryid = $course_category->id;
}
// always place the new items at the end, move them after insert if needed
$last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
if (!empty($last_sortorder)) {
$this->sortorder = $last_sortorder + 1;
} else {
$this->sortorder = 1;
}
// add proper item numbers to manual items
if ($this->itemtype == 'manual') {
if (empty($this->itemnumber)) {
$this->itemnumber = 0;
}
}
// make sure there is not 0 in outcomeid
if (empty($this->outcomeid)) {
$this->outcomeid = null;
}
$this->timecreated = $this->timemodified = time();
if (parent::insert($source)) {
// force regrading of items if needed
$this->force_regrading();
return $this->id;
} else {
debugging("Could not insert this grade_item in the database!");
return false;
}
}
/**
* Set idnumber of grade item, updates also course_modules table
*
* @param string $idnumber (without magic quotes)
* @return bool success
*/
public function add_idnumber($idnumber) {
global $DB;
if (!empty($this->idnumber)) {
return false;
}
if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
if ($this->itemnumber == 0) {
// for activity modules, itemnumber 0 is synced with the course_modules
if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
return false;
}
if (!empty($cm->idnumber)) {
return false;
}
$DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
$this->idnumber = $idnumber;
return $this->update();
} else {
$this->idnumber = $idnumber;
return $this->update();
}
} else {
$this->idnumber = $idnumber;
return $this->update();
}
}
/**
* Returns the locked state of this grade_item (if the grade_item is locked OR no specific
* $userid is given) or the locked state of a specific grade within this item if a specific
* $userid is given and the grade_item is unlocked.
*
* @param int $userid The user's ID
* @return bool Locked state
*/
public function is_locked($userid=NULL) {
global $CFG;
// Override for any grade items belonging to activities which are in the process of being deleted.
require_once($CFG->dirroot . '/course/lib.php');
if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
return true;
}
if (!empty($this->locked)) {
return true;
}
if (!empty($userid)) {
if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
$grade->grade_item =& $this; // prevent db fetching of cached grade_item
return $grade->is_locked();
}
}
return false;
}
/**
* Locks or unlocks this grade_item and (optionally) all its associated final grades.
*
* @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
* @param bool $cascade Lock/unlock child objects too
* @param bool $refresh Refresh grades when unlocking
* @return bool True if grade_item all grades updated, false if at least one update fails
*/
public function set_locked($lockedstate, $cascade=false, $refresh=true) {
if ($lockedstate) {
/// setting lock
if ($this->needsupdate) {
return false; // can not lock grade without first having final grade
}
$this->locked = time();
$this->update();
if ($cascade) {
$grades = $this->get_final();
foreach($grades as $g) {
$grade = new grade_grade($g, false);
$grade->grade_item =& $this;
$grade->set_locked(1, null, false);
}
}
return true;
} else {
/// removing lock
if (!empty($this->locked) and $this->locktime < time()) {
//we have to reset locktime or else it would lock up again
$this->locktime = 0;
}
$this->locked = 0;
$this->update();
if ($cascade) {
if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
foreach($grades as $grade) {
$grade->grade_item =& $this;
$grade->set_locked(0, null, false);
}
}
}
if ($refresh) {
//refresh when unlocking
$this->refresh_grades();
}
return true;
}
}
/**
* Lock the grade if needed. Make sure this is called only when final grades are valid
*/
public function check_locktime() {
if (!empty($this->locked)) {
return; // already locked
}
if ($this->locktime and $this->locktime < time()) {
$this->locked = time();
$this->update('locktime');
}
}
/**
* Set the locktime for this grade item.
*
* @param int $locktime timestamp for lock to activate
* @return void
*/
public function set_locktime($locktime) {
$this->locktime = $locktime;
$this->update();
}
/**
* Set the locktime for this grade item.
*
* @return int $locktime timestamp for lock to activate
*/
public function get_locktime() {
return $this->locktime;
}
/**
* Set the hidden status of grade_item and all grades.
*
* 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
*
* @param int $hidden new hidden status
* @param bool $cascade apply to child objects too
*/
public function set_hidden($hidden, $cascade=false) {
parent::set_hidden($hidden, $cascade);
if ($cascade) {
if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
foreach($grades as $grade) {
$grade->grade_item =& $this;
$grade->set_hidden($hidden, $cascade);
}
}
}
//if marking item visible make sure category is visible MDL-21367
if( !$hidden ) {
$category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
if ($category_array && array_key_exists($this->categoryid, $category_array)) {
$category = $category_array[$this->categoryid];
//call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
//if($category->is_hidden()) {
$category->set_hidden($hidden, false);
//}
}
}
}
/**
* Returns the number of grades that are hidden
*
* @param string $groupsql SQL to limit the query by group
* @param array $params SQL params for $groupsql
* @param string $groupwheresql Where conditions for $groupsql
* @return int The number of hidden grades
*/
public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
global $DB;
$params = (array)$params;
$params['itemid'] = $this->id;
return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
}
/**
* Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
* Situations such as an error being found will still result in the regrading being finished.
*/
public function regrading_finished() {
global $DB;
$this->needsupdate = 0;
//do not use $this->update() because we do not want this logged in grade_item_history
$DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
}
/**
* Performs the necessary calculations on the grades_final referenced by this grade_item.
* Also resets the needsupdate flag once successfully performed.
*
* This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
* because the regrading must be done in correct order!!
*
* @param int $userid Supply a user ID to limit the regrading to a single user
* @return bool true if ok, error string otherwise
*/
public function regrade_final_grades($userid=null) {
global $CFG, $DB;
// locked grade items already have correct final grades
if ($this->is_locked()) {
return true;
}
// calculation produces final value using formula from other final values
if ($this->is_calculated()) {
if ($this->compute($userid)) {
return true;
} else {
return "Could not calculate grades for grade item"; // TODO: improve and localize
}
// noncalculated outcomes already have final values - raw grades not used
} else if ($this->is_outcome_item()) {
return true;
// aggregate the category grade
} else if ($this->is_category_item() or $this->is_course_item()) {
// aggregate category grade item
$category = $this->load_item_category();
$category->grade_item =& $this;
if ($category->generate_grades($userid)) {
return true;
} else {
return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
}
} else if ($this->is_manual_item()) {
// manual items track only final grades, no raw grades
return true;
} else if (!$this->is_raw_used()) {
// hmm - raw grades are not used- nothing to regrade
return true;
}
// normal grade item - just new final grades
$result = true;
$grade_inst = new grade_grade();
$fields = implode(',', $grade_inst->required_fields);
if ($userid) {
$params = array($this->id, $userid);
$rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
} else {
$rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
}
if ($rs) {
foreach ($rs as $grade_record) {
$grade = new grade_grade($grade_record, false);
if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
// this grade is locked - final grade must be ok
continue;
}
$grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
$success = $grade->update('system');
// If successful trigger a user_graded event.
if ($success) {
$grade->load_grade_item();
\core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
} else {
$result = "Internal error updating final grade";
}
}
}
$rs->close();
}
return $result;
}
/**
* Given a float grade value or integer grade scale, applies a number of adjustment based on
* grade_item variables and returns the result.
*
* @param float $rawgrade The raw grade value
* @param float $rawmin original rawmin
* @param float $rawmax original rawmax
* @return mixed
*/
public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
if (is_null($rawgrade)) {
return null;
}
if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
if ($this->grademax < $this->grademin) {
return null;
}
if ($this->grademax == $this->grademin) {
return $this->grademax; // no range
}
// Standardise score to the new grade range
// NOTE: skip if the activity provides a manual rescaling option.
$manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
$rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
}
// Apply other grade_item factors
$rawgrade *= $this->multfactor;
$rawgrade += $this->plusfactor;
return $this->bounded_grade($rawgrade);
} else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
if (empty($this->scale)) {
$this->load_scale();
}
if ($this->grademax < 0) {
return null; // scale not present - no grade
}
if ($this->grademax == 0) {
return $this->grademax; // only one option
}
// Convert scale if needed
// NOTE: skip if the activity provides a manual rescaling option.
$manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
// This should never happen because scales are locked if they are in use.
$rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
}
return $this->bounded_grade($rawgrade);
} else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
// somebody changed the grading type when grades already existed
return null;
} else {
debugging("Unknown grade type");
return null;
}
}
/**
* Update the rawgrademax and rawgrademin for all grade_grades records for this item.
* Scale every rawgrade to maintain the percentage. This function should be called
* after the gradeitem has been updated to the new min and max values.
*
* @param float $oldgrademin The previous grade min value
* @param float $oldgrademax The previous grade max value
* @param float $newgrademin The new grade min value
* @param float $newgrademax The new grade max value
* @param string $source from where was the object inserted (mod/forum, manual, etc.)
* @return bool True on success
*/
public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
global $DB;
if (empty($this->id)) {
return false;
}
if ($oldgrademax <= $oldgrademin) {
// Grades cannot be scaled.
return false;
}
$scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
if (($newgrademax - $newgrademin) <= 1) {
// We would lose too much precision, lets bail.
return false;
}
$rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
foreach ($rs as $graderecord) {
// For each record, create an object to work on.
$grade = new grade_grade($graderecord, false);
// Set this object in the item so it doesn't re-fetch it.
$grade->grade_item = $this;
if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
// Updating the raw grade automatically updates the min/max.
if ($this->is_raw_used()) {
$rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
$this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
} else {
$finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
$this->update_final_grade($grade->userid, $finalgrade, $source);
}
}
}
$rs->close();
// Mark this item for regrading.
$this->force_regrading();
return true;
}
/**
* Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
*
* @return void
*/
public function force_regrading() {
global $DB;
$this->needsupdate = 1;
//mark this item and course item only - categories and calculated items are always regraded
$wheresql = "(itemtype='course' OR id=?) AND courseid=?";
$params = array($this->id, $this->courseid);
$DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
}
/**
* Instantiates a grade_scale object from the DB if this item's scaleid variable is set
*
* @return grade_scale Returns a grade_scale object or null if no scale used
*/
public function load_scale() {
if ($this->gradetype != GRADE_TYPE_SCALE) {
$this->scaleid = null;
}
if (!empty($this->scaleid)) {
//do not load scale if already present
if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
$this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
if (!$this->scale) {
debugging('Incorrect scale id: '.$this->scaleid);
$this->scale = null;
return null;
}
$this->scale->load_items();
}
// Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
// stay with the current min=1 max=count(scaleitems)
$this->grademax = count($this->scale->scale_items);
$this->grademin = 1;
} else {
$this->scale = null;
}
return $this->scale;
}
/**
* Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
*
* @return grade_outcome This grade item's associated grade_outcome or null
*/
public function load_outcome() {
if (!empty($this->outcomeid)) {
$this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
}
return $this->outcome;
}
/**
* Returns the grade_category object this grade_item belongs to (referenced by categoryid)
* or category attached to category item.
*
* @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
*/
public function get_parent_category() {
if ($this->is_category_item() or $this->is_course_item()) {
return $this->get_item_category();
} else {
return grade_category::fetch(array('id'=>$this->categoryid));
}
}
/**
* Calls upon the get_parent_category method to retrieve the grade_category object
* from the DB and assigns it to $this->parent_category. It also returns the object.
*
* @return grade_category This grade item's parent grade_category.
*/
public function load_parent_category() {
if (empty($this->parent_category->id)) {
$this->parent_category = $this->get_parent_category();