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/**
18 * Definition of grade outcome class
19 *
20 * @package   core_grades
21 * @category  grade
22 * @copyright 2006 Nicolas Connault
23 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27
28require_once('grade_object.php');
29
30/**
31 * Class representing a grade outcome.
32 *
33 * It is responsible for handling its DB representation, modifying and returning its metadata.
34 *
35 * @package   core_grades
36 * @category  grade
37 * @copyright 2006 Nicolas Connault
38 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
40class grade_outcome extends grade_object {
41    /**
42     * DB Table (used by grade_object).
43     * @var string $table
44     */
45    public $table = 'grade_outcomes';
46
47    /**
48     * Array of required table fields, must start with 'id'.
49     * @var array $required_fields
50     */
51    public $required_fields = array('id', 'courseid', 'shortname', 'fullname', 'scaleid','description',
52                                 'descriptionformat', 'timecreated', 'timemodified', 'usermodified');
53
54    /**
55     * The course this outcome belongs to.
56     * @var int $courseid
57     */
58    public $courseid;
59
60    /**
61     * The shortname of the outcome.
62     * @var string $shortname
63     */
64    public $shortname;
65
66    /**
67     * The fullname of the outcome.
68     * @var string $fullname
69     */
70    public $fullname;
71
72    /**
73     * A full grade_scale object referenced by $this->scaleid.
74     * @var object $scale
75     */
76    public $scale;
77
78    /**
79     * The id of the scale referenced by this outcome.
80     * @var int $scaleid
81     */
82    public $scaleid;
83
84    /**
85     * The description of this outcome - FORMAT_MOODLE.
86     * @var string $description
87     */
88    public $description;
89
90    /**
91     * The userid of the person who last modified this outcome.
92     *
93     * @var int $usermodified
94     */
95    public $usermodified;
96
97    /**
98     * Deletes this outcome from the database.
99     *
100     * @param string $source from where was the object deleted (mod/forum, manual, etc.)
101     * @return bool success
102     */
103    public function delete($source=null) {
104        global $DB;
105        if (!empty($this->courseid)) {
106            $DB->delete_records('grade_outcomes_courses', array('outcomeid' => $this->id, 'courseid' => $this->courseid));
107        }
108        if (parent::delete($source)) {
109            $context = context_system::instance();
110            $fs = get_file_storage();
111            $files = $fs->get_area_files($context->id, 'grade', 'outcome', $this->id);
112            foreach ($files as $file) {
113                $file->delete();
114            }
115            return true;
116        }
117        return false;
118    }
119
120    /**
121     * Records this object in the Database, sets its id to the returned value, and returns that value.
122     * If successful this function also fetches the new object data from database and stores it
123     * in object properties.
124     *
125     * @param string $source from where was the object inserted (mod/forum, manual, etc.)
126     * @return int PK ID if successful, false otherwise
127     */
128    public function insert($source=null) {
129        global $DB;
130
131        $this->timecreated = $this->timemodified = time();
132
133        if ($result = parent::insert($source)) {
134            if (!empty($this->courseid)) {
135                $goc = new stdClass();
136                $goc->courseid = $this->courseid;
137                $goc->outcomeid = $this->id;
138                $DB->insert_record('grade_outcomes_courses', $goc);
139            }
140        }
141        return $result;
142    }
143
144    /**
145     * In addition to update() it also updates grade_outcomes_courses if needed
146     *
147     * @param string $source from where was the object inserted
148     * @return bool success
149     */
150    public function update($source=null) {
151        $this->timemodified = time();
152
153        if ($result = parent::update($source)) {
154            if (!empty($this->courseid)) {
155                $this->use_in($this->courseid);
156            }
157        }
158        return $result;
159    }
160
161    /**
162     * Mark outcome as used in a course
163     *
164     * @param int $courseid
165     * @return False if invalid courseid requested
166     */
167    public function use_in($courseid) {
168        global $DB;
169        if (!empty($this->courseid) and $courseid != $this->courseid) {
170            return false;
171        }
172
173        if (!$DB->record_exists('grade_outcomes_courses', array('courseid' => $courseid, 'outcomeid' => $this->id))) {
174            $goc = new stdClass();
175            $goc->courseid  = $courseid;
176            $goc->outcomeid = $this->id;
177            $DB->insert_record('grade_outcomes_courses', $goc);
178        }
179        return true;
180    }
181
182    /**
183     * Finds and returns a grade_outcome instance based on params.
184     *
185     * @static
186     * @param array $params associative arrays varname=>value
187     * @return object grade_outcome instance or false if none found.
188     */
189    public static function fetch($params) {
190        return grade_object::fetch_helper('grade_outcomes', 'grade_outcome', $params);
191    }
192
193    /**
194     * Finds and returns all grade_outcome instances based on params.
195     *
196     * @static
197     * @param array $params associative arrays varname=>value
198     * @return array array of grade_outcome insatnces or false if none found.
199     */
200    public static function fetch_all($params) {
201        return grade_object::fetch_all_helper('grade_outcomes', 'grade_outcome', $params);
202    }
203
204    /**
205     * Instantiates a grade_scale object whose data is retrieved from the database
206     *
207     * @return grade_scale
208     */
209    public function load_scale() {
210        if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
211            $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
212            $this->scale->load_items();
213        }
214        return $this->scale;
215    }
216
217    /**
218     * Static function returning all global outcomes
219     *
220     * @static
221     * @return array
222     */
223    public static function fetch_all_global() {
224        if (!$outcomes = grade_outcome::fetch_all(array('courseid'=>null))) {
225            $outcomes = array();
226        }
227        return $outcomes;
228    }
229
230    /**
231     * Static function returning all local course outcomes
232     *
233     * @static
234     * @param int $courseid
235     * @return array
236     */
237    public static function fetch_all_local($courseid) {
238        if (!$outcomes =grade_outcome::fetch_all(array('courseid'=>$courseid))) {
239            $outcomes = array();
240        }
241        return $outcomes;
242    }
243
244    /**
245     * Static method that returns all outcomes available in course
246     *
247     * @static
248     * @param int $courseid
249     * @return array
250     */
251    public static function fetch_all_available($courseid) {
252        global $CFG, $DB;
253
254        $result = array();
255        $params = array($courseid);
256        $sql = "SELECT go.*
257                  FROM {grade_outcomes} go, {grade_outcomes_courses} goc
258                 WHERE go.id = goc.outcomeid AND goc.courseid = ?
259              ORDER BY go.id ASC";
260
261        if ($datas = $DB->get_records_sql($sql, $params)) {
262            foreach($datas as $data) {
263                $instance = new grade_outcome();
264                grade_object::set_properties($instance, $data);
265                $result[$instance->id] = $instance;
266            }
267        }
268        return $result;
269    }
270
271
272    /**
273     * Returns the most descriptive field for this object. This is a standard method used
274     * when we do not know the exact type of an object.
275     *
276     * @return string name
277     */
278    public function get_name() {
279        // Grade outcomes can be created at site or course context, so set the filter context appropriately.
280        $context = empty($this->courseid) ? context_system::instance() : context_course::instance($this->courseid);
281        return format_string($this->fullname, false, ["context" => $context]);
282    }
283
284    /**
285     * Returns unique outcome short name.
286     *
287     * @return string name
288     */
289    public function get_shortname() {
290        return $this->shortname;
291    }
292
293    /**
294     * Returns the formatted grade description with URLs converted
295     *
296     * @return string
297     */
298    public function get_description() {
299        global $CFG;
300        require_once($CFG->libdir . '/filelib.php');
301
302        $options = new stdClass;
303        $options->noclean = true;
304        $systemcontext = context_system::instance();
305        $description = file_rewrite_pluginfile_urls($this->description, 'pluginfile.php', $systemcontext->id, 'grade', 'outcome', $this->id);
306        return format_text($description, $this->descriptionformat, $options);
307    }
308
309    /**
310     * Checks if outcome can be deleted.
311     *
312     * @return bool
313     */
314    public function can_delete() {
315        if ($this->get_item_uses_count()) {
316            return false;
317        }
318        if (empty($this->courseid)) {
319            if ($this->get_course_uses_count()) {
320                return false;
321            }
322        }
323        return true;
324    }
325
326    /**
327     * Returns the number of places where outcome is used.
328     *
329     * @return int
330     */
331    public function get_course_uses_count() {
332        global $DB;
333
334        if (!empty($this->courseid)) {
335            return 1;
336        }
337
338        return $DB->count_records('grade_outcomes_courses', array('outcomeid' => $this->id));
339    }
340
341    /**
342     * Returns the number of grade items that use this grade outcome
343     *
344     * @return int
345     */
346    public function get_item_uses_count() {
347        global $DB;
348        return $DB->count_records('grade_items', array('outcomeid' => $this->id));
349    }
350
351    /**
352     * Computes then returns extra information about this outcome and other objects that are linked to it.
353     * The average of all grades that use this outcome, for all courses (or 1 course if courseid is given) can
354     * be requested, and is returned as a float if requested alone. If the list of items that use this outcome
355     * is also requested, then a single array is returned, which contains the grade_items AND the average grade
356     * if such is still requested (array('items' => array(...), 'avg' => 2.30)). This combining of two
357     * methods into one is to save on DB queries, since both queries are similar and can be performed together.
358     *
359     * @param int $courseid An optional courseid to narrow down the average to 1 course only
360     * @param bool $average Whether or not to return the average grade for this outcome
361     * @param bool $items Whether or not to return the list of items using this outcome
362     * @return float
363     */
364    public function get_grade_info($courseid=null, $average=true, $items=false) {
365        global $CFG, $DB;
366
367        if (!isset($this->id)) {
368            debugging("You must setup the outcome's id before calling its get_grade_info() method!");
369            return false; // id must be defined for this to work
370        }
371
372        if ($average === false && $items === false) {
373            debugging('Either the 1st or 2nd param of grade_outcome::get_grade_info() must be true, or both, but not both false!');
374            return false;
375        }
376
377        $params = array($this->id);
378
379        $wheresql = '';
380        if (!is_null($courseid)) {
381            $wheresql = " AND {grade_items}.courseid = ? ";
382            $params[] = $courseid;
383        }
384
385        $selectadd = '';
386        if ($items !== false) {
387            $selectadd = ", {grade_items}.* ";
388        }
389
390        $sql = "SELECT finalgrade $selectadd
391                  FROM {grade_grades}, {grade_items}, {grade_outcomes}
392                 WHERE {grade_outcomes}.id = {grade_items}.outcomeid
393                   AND {grade_items}.id = {grade_grades}.itemid
394                   AND {grade_outcomes}.id = ?
395                   $wheresql";
396
397        $grades = $DB->get_records_sql($sql, $params);
398        $retval = array();
399
400        if ($average !== false && count($grades) > 0) {
401            $count = 0;
402            $total = 0;
403
404            foreach ($grades as $k => $grade) {
405                // Skip null finalgrades
406                if (!is_null($grade->finalgrade)) {
407                    $total += $grade->finalgrade;
408                    $count++;
409                }
410                unset($grades[$k]->finalgrade);
411            }
412
413            $retval['avg'] = $total / $count;
414        }
415
416        if ($items !== false) {
417            foreach ($grades as $grade) {
418                $retval['items'][$grade->id] = new grade_item($grade);
419            }
420        }
421
422        return $retval;
423    }
424}
425