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 a grade object class for grade item, grade category etc to inherit from
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
28/**
29 * An abstract object that holds methods and attributes common to all grade_* objects defined here.
30 *
31 * @package   core_grades
32 * @category  grade
33 * @copyright 2006 Nicolas Connault
34 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36abstract class grade_object {
37    /**
38     * The database table this grade object is stored in
39     * @var string $table
40     */
41    public $table;
42
43    /**
44     * Array of required table fields, must start with 'id'.
45     * @var array $required_fields
46     */
47    public $required_fields = array('id', 'timecreated', 'timemodified', 'hidden');
48
49    /**
50     * Array of optional fields with default values - usually long text information that is not always needed.
51     * If you want to create an instance without optional fields use: new grade_object($only_required_fields, false);
52     * @var array $optional_fields
53     */
54    public $optional_fields = array();
55
56    /**
57     * The PK.
58     * @var int $id
59     */
60    public $id;
61
62    /**
63     * The first time this grade_object was created.
64     * @var int $timecreated
65     */
66    public $timecreated;
67
68    /**
69     * The last time this grade_object was modified.
70     * @var int $timemodified
71     */
72    public $timemodified;
73
74    /**
75     * 0 if visible, 1 always hidden or date not visible until
76     * @var int $hidden
77     */
78    var $hidden = 0;
79
80    /**
81     * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
82     *
83     * @param array $params An array with required parameters for this grade object.
84     * @param bool $fetch Whether to fetch corresponding row from the database or not,
85     *        optional fields might not be defined if false used
86     */
87    public function __construct($params=NULL, $fetch=true) {
88        if (!empty($params) and (is_array($params) or is_object($params))) {
89            if ($fetch) {
90                if ($data = $this->fetch($params)) {
91                    grade_object::set_properties($this, $data);
92                } else {
93                    grade_object::set_properties($this, $this->optional_fields);//apply defaults for optional fields
94                    grade_object::set_properties($this, $params);
95                }
96
97            } else {
98                grade_object::set_properties($this, $params);
99            }
100
101        } else {
102            grade_object::set_properties($this, $this->optional_fields);//apply defaults for optional fields
103        }
104    }
105
106    /**
107     * Makes sure all the optional fields are loaded.
108     *
109     * If id present, meaning the instance exists in the database, then data will be fetched from the database.
110     * Defaults are used for new instances.
111     */
112    public function load_optional_fields() {
113        global $DB;
114        foreach ($this->optional_fields as $field=>$default) {
115            if (property_exists($this, $field)) {
116                continue;
117            }
118            if (empty($this->id)) {
119                $this->$field = $default;
120            } else {
121                $this->$field = $DB->get_field($this->table, $field, array('id', $this->id));
122            }
123        }
124    }
125
126    /**
127     * Finds and returns a grade_object instance based on params.
128     *
129     * @static
130     * @abstract
131     * @param array $params associative arrays varname=>value
132     * @return object grade_object instance or false if none found.
133     */
134    public static function fetch($params) {
135        throw new coding_exception('fetch() method needs to be overridden in each subclass of grade_object');
136    }
137
138    /**
139     * Finds and returns all grade_object instances based on $params.
140     *
141     * @static
142     * @abstract
143     * @throws coding_exception Throws a coding exception if fetch_all() has not been overriden by the grade object subclass
144     * @param array $params Associative arrays varname=>value
145     * @return array|bool Array of grade_object instances or false if none found.
146     */
147    public static function fetch_all($params) {
148        throw new coding_exception('fetch_all() method needs to be overridden in each subclass of grade_object');
149    }
150
151    /**
152     * Factory method which uses the parameters to retrieve matching instances from the database
153     *
154     * @param string $table The table to retrieve from
155     * @param string $classname The name of the class to instantiate
156     * @param array $params An array of conditions like $fieldname => $fieldvalue
157     * @return mixed An object instance or false if not found
158     */
159    protected static function fetch_helper($table, $classname, $params) {
160        if ($instances = grade_object::fetch_all_helper($table, $classname, $params)) {
161            if (count($instances) > 1) {
162                // we should not tolerate any errors here - problems might appear later
163                print_error('morethanonerecordinfetch','debug');
164            }
165            return reset($instances);
166        } else {
167            return false;
168        }
169    }
170
171    /**
172     * Factory method which uses the parameters to retrieve all matching instances from the database
173     *
174     * @param string $table The table to retrieve from
175     * @param string $classname The name of the class to instantiate
176     * @param array $params An array of conditions like $fieldname => $fieldvalue
177     * @return array|bool Array of object instances or false if not found
178     */
179    public static function fetch_all_helper($table, $classname, $params) {
180        global $DB; // Need to introspect DB here.
181
182        $instance = new $classname();
183
184        $classvars = (array)$instance;
185        $params    = (array)$params;
186
187        $wheresql = array();
188        $newparams = array();
189
190        $columns = $DB->get_columns($table); // Cached, no worries.
191
192        foreach ($params as $var=>$value) {
193            if (!in_array($var, $instance->required_fields) and !array_key_exists($var, $instance->optional_fields)) {
194                continue;
195            }
196            if (!array_key_exists($var, $columns)) {
197                continue;
198            }
199            if (is_null($value)) {
200                $wheresql[] = " $var IS NULL ";
201            } else {
202                if ($columns[$var]->meta_type === 'X') {
203                    // We have a text/clob column, use the cross-db method for its comparison.
204                    $wheresql[] = ' ' . $DB->sql_compare_text($var) . ' = ' . $DB->sql_compare_text('?') . ' ';
205                } else {
206                    // Other columns (varchar, integers...).
207                    $wheresql[] = " $var = ? ";
208                }
209                $newparams[] = $value;
210            }
211        }
212
213        if (empty($wheresql)) {
214            $wheresql = '';
215        } else {
216            $wheresql = implode("AND", $wheresql);
217        }
218
219        global $DB;
220        $rs = $DB->get_recordset_select($table, $wheresql, $newparams);
221        //returning false rather than empty array if nothing found
222        if (!$rs->valid()) {
223            $rs->close();
224            return false;
225        }
226
227        $result = array();
228        foreach($rs as $data) {
229            $instance = new $classname();
230            grade_object::set_properties($instance, $data);
231            $result[$instance->id] = $instance;
232        }
233        $rs->close();
234        return $result;
235    }
236
237    /**
238     * Updates this object in the Database, based on its object variables. ID must be set.
239     *
240     * @param string $source from where was the object updated (mod/forum, manual, etc.)
241     * @return bool success
242     */
243    public function update($source=null) {
244        global $USER, $CFG, $DB;
245
246        if (empty($this->id)) {
247            debugging('Can not update grade object, no id!');
248            return false;
249        }
250
251        $data = $this->get_record_data();
252
253        $DB->update_record($this->table, $data);
254
255        $historyid = null;
256        if (empty($CFG->disablegradehistory)) {
257            unset($data->timecreated);
258            $data->action       = GRADE_HISTORY_UPDATE;
259            $data->oldid        = $this->id;
260            $data->source       = $source;
261            $data->timemodified = time();
262            $data->loggeduser   = $USER->id;
263            $historyid = $DB->insert_record($this->table.'_history', $data);
264        }
265
266        $this->notify_changed(false);
267
268        $this->update_feedback_files($historyid);
269
270        return true;
271    }
272
273    /**
274     * Deletes this object from the database.
275     *
276     * @param string $source From where was the object deleted (mod/forum, manual, etc.)
277     * @return bool success
278     */
279    public function delete($source=null) {
280        global $USER, $CFG, $DB;
281
282        if (empty($this->id)) {
283            debugging('Can not delete grade object, no id!');
284            return false;
285        }
286
287        $data = $this->get_record_data();
288
289        if ($DB->delete_records($this->table, array('id'=>$this->id))) {
290            if (empty($CFG->disablegradehistory)) {
291                unset($data->id);
292                unset($data->timecreated);
293                $data->action       = GRADE_HISTORY_DELETE;
294                $data->oldid        = $this->id;
295                $data->source       = $source;
296                $data->timemodified = time();
297                $data->loggeduser   = $USER->id;
298                $DB->insert_record($this->table.'_history', $data);
299            }
300
301            $this->notify_changed(true);
302
303            $this->delete_feedback_files();
304
305            return true;
306        } else {
307            return false;
308        }
309    }
310
311    /**
312     * Returns object with fields and values that are defined in database
313     *
314     * @return stdClass
315     */
316    public function get_record_data() {
317        $data = new stdClass();
318
319        foreach ($this as $var=>$value) {
320            if (in_array($var, $this->required_fields) or array_key_exists($var, $this->optional_fields)) {
321                if (is_object($value) or is_array($value)) {
322                    debugging("Incorrect property '$var' found when inserting grade object");
323                } else {
324                    $data->$var = $value;
325                }
326            }
327        }
328        return $data;
329    }
330
331    /**
332     * Records this object in the Database, sets its id to the returned value, and returns that value.
333     * If successful this function also fetches the new object data from database and stores it
334     * in object properties.
335     *
336     * @param string $source From where was the object inserted (mod/forum, manual, etc.)
337     * @return int The new grade object ID if successful, false otherwise
338     */
339    public function insert($source=null) {
340        global $USER, $CFG, $DB;
341
342        if (!empty($this->id)) {
343            debugging("Grade object already exists!");
344            return false;
345        }
346
347        $data = $this->get_record_data();
348
349        $this->id = $DB->insert_record($this->table, $data);
350
351        // set all object properties from real db data
352        $this->update_from_db();
353
354        $data = $this->get_record_data();
355
356        $historyid = null;
357        if (empty($CFG->disablegradehistory)) {
358            unset($data->timecreated);
359            $data->action       = GRADE_HISTORY_INSERT;
360            $data->oldid        = $this->id;
361            $data->source       = $source;
362            $data->timemodified = time();
363            $data->loggeduser   = $USER->id;
364            $historyid = $DB->insert_record($this->table.'_history', $data);
365        }
366
367        $this->notify_changed(false);
368
369        $this->add_feedback_files($historyid);
370
371        return $this->id;
372    }
373
374    /**
375     * Using this object's id field, fetches the matching record in the DB, and looks at
376     * each variable in turn. If the DB has different data, the db's data is used to update
377     * the object. This is different from the update() function, which acts on the DB record
378     * based on the object.
379     *
380     * @return bool True if successful
381     */
382    public function update_from_db() {
383        if (empty($this->id)) {
384            debugging("The object could not be used in its state to retrieve a matching record from the DB, because its id field is not set.");
385            return false;
386        }
387        global $DB;
388        if (!$params = $DB->get_record($this->table, array('id' => $this->id))) {
389            debugging("Object with this id:{$this->id} does not exist in table:{$this->table}, can not update from db!");
390            return false;
391        }
392
393        grade_object::set_properties($this, $params);
394
395        return true;
396    }
397
398    /**
399     * Given an associated array or object, cycles through each key/variable
400     * and assigns the value to the corresponding variable in this object.
401     *
402     * @param stdClass $instance The object to set the properties on
403     * @param array $params An array of properties to set like $propertyname => $propertyvalue
404     * @return array|stdClass Either an associative array or an object containing property name, property value pairs
405     */
406    public static function set_properties(&$instance, $params) {
407        $params = (array) $params;
408        foreach ($params as $var => $value) {
409            if (in_array($var, $instance->required_fields) or array_key_exists($var, $instance->optional_fields)) {
410                $instance->$var = $value;
411            }
412        }
413    }
414
415    /**
416     * Called immediately after the object data has been inserted, updated, or
417     * deleted in the database. Default does nothing, can be overridden to
418     * hook in special behaviour.
419     *
420     * @param bool $deleted
421     */
422    protected function notify_changed($deleted) {
423    }
424
425    /**
426     * Handles adding feedback files in the gradebook.
427     *
428     * @param int|null $historyid
429     */
430    protected function add_feedback_files(int $historyid = null) {
431    }
432
433    /**
434     * Handles updating feedback files in the gradebook.
435     *
436     * @param int|null $historyid
437     */
438    protected function update_feedback_files(int $historyid = null) {
439    }
440
441    /**
442     * Handles deleting feedback files in the gradebook.
443     */
444    protected function delete_feedback_files() {
445    }
446
447    /**
448     * Returns the current hidden state of this grade_item
449     *
450     * This depends on the grade object hidden setting and the current time if hidden is set to a "hidden until" timestamp
451     *
452     * @return bool Current hidden state
453     */
454    function is_hidden() {
455        return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
456    }
457
458    /**
459     * Check grade object hidden status
460     *
461     * @return bool True if a "hidden until" timestamp is set, false if grade object is set to always visible or always hidden.
462     */
463    function is_hiddenuntil() {
464        return $this->hidden > 1;
465    }
466
467    /**
468     * Check a grade item hidden status.
469     *
470     * @return int 0 means visible, 1 hidden always, a timestamp means "hidden until"
471     */
472    function get_hidden() {
473        return $this->hidden;
474    }
475
476    /**
477     * Set a grade object hidden status
478     *
479     * @param int $hidden 0 means visiable, 1 means hidden always, a timestamp means "hidden until"
480     * @param bool $cascade Ignored
481     */
482    function set_hidden($hidden, $cascade=false) {
483        $this->hidden = $hidden;
484        $this->update();
485    }
486
487    /**
488     * Returns whether the grade object can control the visibility of the grades.
489     *
490     * @return bool
491     */
492    public function can_control_visibility() {
493        return true;
494    }
495}
496