MDL-54613 unit tests: Add iteminstance to test grade_item
[moodle.git] / lib / grade / grade_object.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
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  */
36 abstract class grade_object {
37     /**
38      * The database table this grade object is stored in
39      * @var string $table
40      */
41     public $table;
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');
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();
56     /**
57      * The PK.
58      * @var int $id
59      */
60     public $id;
62     /**
63      * The first time this grade_object was created.
64      * @var int $timecreated
65      */
66     public $timecreated;
68     /**
69      * The last time this grade_object was modified.
70      * @var int $timemodified
71      */
72     public $timemodified;
74     /**
75      * 0 if visible, 1 always hidden or date not visible until
76      * @var int $hidden
77      */
78     var $hidden = 0;
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                 }
97             } else {
98                 grade_object::set_properties($this, $params);
99             }
101         } else {
102             grade_object::set_properties($this, $this->optional_fields);//apply defaults for optional fields
103         }
104     }
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     }
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     }
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     }
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     }
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.
182         $instance = new $classname();
184         $classvars = (array)$instance;
185         $params    = (array)$params;
187         $wheresql = array();
188         $newparams = array();
190         $columns = $DB->get_columns($table); // Cached, no worries.
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         }
213         if (empty($wheresql)) {
214             $wheresql = '';
215         } else {
216             $wheresql = implode("AND", $wheresql);
217         }
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         }
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();
235         return $result;
236     }
238     /**
239      * Updates this object in the Database, based on its object variables. ID must be set.
240      *
241      * @param string $source from where was the object updated (mod/forum, manual, etc.)
242      * @return bool success
243      */
244     public function update($source=null) {
245         global $USER, $CFG, $DB;
247         if (empty($this->id)) {
248             debugging('Can not update grade object, no id!');
249             return false;
250         }
252         $data = $this->get_record_data();
254         $DB->update_record($this->table, $data);
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             $DB->insert_record($this->table.'_history', $data);
264         }
266         $this->notify_changed(false);
267         return true;
268     }
270     /**
271      * Deletes this object from the database.
272      *
273      * @param string $source From where was the object deleted (mod/forum, manual, etc.)
274      * @return bool success
275      */
276     public function delete($source=null) {
277         global $USER, $CFG, $DB;
279         if (empty($this->id)) {
280             debugging('Can not delete grade object, no id!');
281             return false;
282         }
284         $data = $this->get_record_data();
286         if ($DB->delete_records($this->table, array('id'=>$this->id))) {
287             if (empty($CFG->disablegradehistory)) {
288                 unset($data->id);
289                 unset($data->timecreated);
290                 $data->action       = GRADE_HISTORY_DELETE;
291                 $data->oldid        = $this->id;
292                 $data->source       = $source;
293                 $data->timemodified = time();
294                 $data->loggeduser   = $USER->id;
295                 $DB->insert_record($this->table.'_history', $data);
296             }
297             $this->notify_changed(true);
298             return true;
300         } else {
301             return false;
302         }
303     }
305     /**
306      * Returns object with fields and values that are defined in database
307      *
308      * @return stdClass
309      */
310     public function get_record_data() {
311         $data = new stdClass();
313         foreach ($this as $var=>$value) {
314             if (in_array($var, $this->required_fields) or array_key_exists($var, $this->optional_fields)) {
315                 if (is_object($value) or is_array($value)) {
316                     debugging("Incorrect property '$var' found when inserting grade object");
317                 } else {
318                     $data->$var = $value;
319                 }
320             }
321         }
322         return $data;
323     }
325     /**
326      * Records this object in the Database, sets its id to the returned value, and returns that value.
327      * If successful this function also fetches the new object data from database and stores it
328      * in object properties.
329      *
330      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
331      * @return int The new grade object ID if successful, false otherwise
332      */
333     public function insert($source=null) {
334         global $USER, $CFG, $DB;
336         if (!empty($this->id)) {
337             debugging("Grade object already exists!");
338             return false;
339         }
341         $data = $this->get_record_data();
343         $this->id = $DB->insert_record($this->table, $data);
345         // set all object properties from real db data
346         $this->update_from_db();
348         $data = $this->get_record_data();
350         if (empty($CFG->disablegradehistory)) {
351             unset($data->timecreated);
352             $data->action       = GRADE_HISTORY_INSERT;
353             $data->oldid        = $this->id;
354             $data->source       = $source;
355             $data->timemodified = time();
356             $data->loggeduser   = $USER->id;
357             $DB->insert_record($this->table.'_history', $data);
358         }
360         $this->notify_changed(false);
361         return $this->id;
362     }
364     /**
365      * Using this object's id field, fetches the matching record in the DB, and looks at
366      * each variable in turn. If the DB has different data, the db's data is used to update
367      * the object. This is different from the update() function, which acts on the DB record
368      * based on the object.
369      *
370      * @return bool True if successful
371      */
372     public function update_from_db() {
373         if (empty($this->id)) {
374             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.");
375             return false;
376         }
377         global $DB;
378         if (!$params = $DB->get_record($this->table, array('id' => $this->id))) {
379             debugging("Object with this id:{$this->id} does not exist in table:{$this->table}, can not update from db!");
380             return false;
381         }
383         grade_object::set_properties($this, $params);
385         return true;
386     }
388     /**
389      * Given an associated array or object, cycles through each key/variable
390      * and assigns the value to the corresponding variable in this object.
391      *
392      * @param stdClass $instance The object to set the properties on
393      * @param array $params An array of properties to set like $propertyname => $propertyvalue
394      * @return array|stdClass Either an associative array or an object containing property name, property value pairs
395      */
396     public static function set_properties(&$instance, $params) {
397         $params = (array) $params;
398         foreach ($params as $var => $value) {
399             if (in_array($var, $instance->required_fields) or array_key_exists($var, $instance->optional_fields)) {
400                 $instance->$var = $value;
401             }
402         }
403     }
405     /**
406      * Called immediately after the object data has been inserted, updated, or
407      * deleted in the database. Default does nothing, can be overridden to
408      * hook in special behaviour.
409      *
410      * @param bool $deleted
411      */
412     protected function notify_changed($deleted) {
413     }
415     /**
416      * Returns the current hidden state of this grade_item
417      *
418      * This depends on the grade object hidden setting and the current time if hidden is set to a "hidden until" timestamp
419      *
420      * @return bool Current hidden state
421      */
422     function is_hidden() {
423         return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
424     }
426     /**
427      * Check grade object hidden status
428      *
429      * @return bool True if a "hidden until" timestamp is set, false if grade object is set to always visible or always hidden.
430      */
431     function is_hiddenuntil() {
432         return $this->hidden > 1;
433     }
435     /**
436      * Check a grade item hidden status.
437      *
438      * @return int 0 means visible, 1 hidden always, a timestamp means "hidden until"
439      */
440     function get_hidden() {
441         return $this->hidden;
442     }
444     /**
445      * Set a grade object hidden status
446      *
447      * @param int $hidden 0 means visiable, 1 means hidden always, a timestamp means "hidden until"
448      * @param bool $cascade Ignored
449      */
450     function set_hidden($hidden, $cascade=false) {
451         $this->hidden = $hidden;
452         $this->update();
453     }
455     /**
456      * Returns whether the grade object can control the visibility of the grades.
457      *
458      * @return bool
459      */
460     public function can_control_visibility() {
461         return true;
462     }