Commit | Line | Data |
---|---|---|
9b8550f8 DM |
1 | <?php |
2 | ||
3 | // This file is part of Moodle - http://moodle.org/ | |
4 | // | |
5 | // Moodle is free software: you can redistribute it and/or modify | |
6 | // it under the terms of the GNU General Public License as published by | |
7 | // the Free Software Foundation, either version 3 of the License, or | |
8 | // (at your option) any later version. | |
9 | // | |
10 | // Moodle is distributed in the hope that it will be useful, | |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | // GNU General Public License for more details. | |
14 | // | |
15 | // You should have received a copy of the GNU General Public License | |
16 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
17 | ||
18 | /** | |
21d37aa6 DM |
19 | * Common classes used by gradingform plugintypes are defined here |
20 | * | |
9b8550f8 DM |
21 | * @package core |
22 | * @subpackage grading | |
23 | * @copyright 2011 David Mudrak <david@moodle.com> | |
24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25 | */ | |
26 | ||
27 | defined('MOODLE_INTERNAL') || die(); | |
28 | ||
29 | /** | |
21d37aa6 | 30 | * Grading method controller represents a plugin used in a particular area |
9b8550f8 | 31 | */ |
3e43eff5 | 32 | abstract class gradingform_controller { |
9b8550f8 | 33 | |
7622ae95 DM |
34 | /** undefined definition status */ |
35 | const DEFINITION_STATUS_NULL = 0; | |
36 | /** the form is currently being edited and is not ready for usage yet */ | |
37 | const DEFINITION_STATUS_DRAFT = 10; | |
38 | /** the for was marked as ready for actual usage */ | |
39 | const DEFINITION_STATUS_READY = 20; | |
21d37aa6 | 40 | |
9b8550f8 DM |
41 | /** @var stdClass the context */ |
42 | protected $context; | |
43 | ||
44 | /** @var string the frankenstyle name of the component */ | |
45 | protected $component; | |
46 | ||
47 | /** @var string the name of the gradable area */ | |
48 | protected $area; | |
49 | ||
50 | /** @var int the id of the gradable area record */ | |
51 | protected $areaid; | |
52 | ||
fe817d87 | 53 | /** @var stdClass|false the definition structure */ |
5060997b | 54 | protected $definition = false; |
3e43eff5 | 55 | |
9e2eca0f MG |
56 | /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */ |
57 | private $graderange = null; | |
58 | ||
0136124e MG |
59 | /** @var boolean|null cached result of function has_active_instances() */ |
60 | protected $hasactiveinstances = null; | |
61 | ||
9b8550f8 DM |
62 | /** |
63 | * Do not instantinate this directly, use {@link grading_manager::get_controller()} | |
64 | * | |
fe817d87 DM |
65 | * @param stdClass $context the context of the form |
66 | * @param string $component the frankenstyle name of the component | |
67 | * @param string $area the name of the gradable area | |
68 | * @param int $areaid the id of the gradable area record | |
9b8550f8 DM |
69 | */ |
70 | public function __construct(stdClass $context, $component, $area, $areaid) { | |
3e43eff5 DM |
71 | global $DB; |
72 | ||
73 | $this->context = $context; | |
fe817d87 DM |
74 | list($type, $name) = normalize_component($component); |
75 | $this->component = $type.'_'.$name; | |
3e43eff5 DM |
76 | $this->area = $area; |
77 | $this->areaid = $areaid; | |
78 | ||
79 | $this->load_definition(); | |
80 | } | |
81 | ||
fe817d87 DM |
82 | /** |
83 | * @return stdClass controller context | |
84 | */ | |
85 | public function get_context() { | |
86 | return $this->context; | |
87 | } | |
88 | ||
89 | /** | |
90 | * @return string gradable component name | |
91 | */ | |
92 | public function get_component() { | |
93 | return $this->component; | |
94 | } | |
95 | ||
96 | /** | |
97 | * @return string gradable area name | |
98 | */ | |
99 | public function get_area() { | |
100 | return $this->area; | |
101 | } | |
102 | ||
103 | /** | |
104 | * @return int gradable area id | |
105 | */ | |
106 | public function get_areaid() { | |
107 | return $this->areaid; | |
108 | } | |
109 | ||
6832a102 DM |
110 | /** |
111 | * Is the form definition record available? | |
112 | * | |
113 | * Note that this actually checks whether the process of defining the form ever started | |
114 | * and not whether the form definition should be considered as final. | |
115 | * | |
116 | * @return boolean | |
117 | */ | |
118 | public function is_form_defined() { | |
5060997b | 119 | return ($this->definition !== false); |
6832a102 DM |
120 | } |
121 | ||
3e43eff5 | 122 | /** |
7622ae95 | 123 | * Is the grading form defined and ready for usage? |
3e43eff5 DM |
124 | * |
125 | * @return boolean | |
126 | */ | |
7622ae95 | 127 | public function is_form_available() { |
3599b113 | 128 | return ($this->is_form_defined() && $this->definition->status == self::DEFINITION_STATUS_READY); |
3e43eff5 DM |
129 | } |
130 | ||
967d346f DM |
131 | /** |
132 | * Is the grading form saved as a shared public template? | |
133 | * | |
134 | * @return boolean | |
135 | */ | |
136 | public function is_shared_template() { | |
137 | return ($this->get_context()->id == context_system::instance()->id | |
138 | and $this->get_component() == 'core_grading'); | |
139 | } | |
140 | ||
141 | /** | |
142 | * Is the grading form owned by the given user? | |
143 | * | |
144 | * The form owner is the user who created this instance of the form. | |
145 | * | |
146 | * @param int $userid the user id to check, defaults to the current user | |
147 | * @return boolean|null null if the form not defined yet, boolean otherwise | |
148 | */ | |
149 | public function is_own_form($userid = null) { | |
150 | global $USER; | |
151 | ||
152 | if (!$this->is_form_defined()) { | |
153 | return null; | |
154 | } | |
155 | if (is_null($userid)) { | |
156 | $userid = $USER->id; | |
157 | } | |
158 | return ($this->definition->usercreated == $userid); | |
159 | } | |
160 | ||
2ae7faf1 MG |
161 | /** |
162 | * Returns a message why this form is unavailable. Maybe overriden by plugins to give more details. | |
163 | * @see is_form_available() | |
164 | * | |
165 | * @return string | |
166 | */ | |
167 | public function form_unavailable_notification() { | |
168 | if ($this->is_form_available()) { | |
169 | return null; | |
170 | } | |
0e2a1d75 | 171 | return get_string('gradingformunavailable', 'grading'); |
2ae7faf1 MG |
172 | } |
173 | ||
6832a102 DM |
174 | /** |
175 | * Returns URL of a page where the grading form can be defined and edited. | |
176 | * | |
177 | * @param moodle_url $returnurl optional URL of a page where the user should be sent once they are finished with editing | |
178 | * @return moodle_url | |
179 | */ | |
180 | public function get_editor_url(moodle_url $returnurl = null) { | |
181 | ||
182 | $params = array('areaid' => $this->areaid); | |
183 | ||
184 | if (!is_null($returnurl)) { | |
185 | $params['returnurl'] = $returnurl->out(false); | |
186 | } | |
187 | ||
188 | return new moodle_url('/grade/grading/form/'.$this->get_method_name().'/edit.php', $params); | |
189 | } | |
190 | ||
9b8550f8 DM |
191 | /** |
192 | * Extends the module settings navigation | |
193 | * | |
194 | * This function is called when the context for the page is an activity module with the | |
195 | * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms | |
196 | * and there is an area with the active grading method set to the given plugin. | |
197 | * | |
198 | * @param settings_navigation $settingsnav {@link settings_navigation} | |
199 | * @param navigation_node $node {@link navigation_node} | |
200 | */ | |
201 | public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) { | |
202 | // do not extend by default | |
203 | } | |
3e43eff5 DM |
204 | |
205 | /** | |
fe817d87 | 206 | * Returns the grading form definition structure |
3e43eff5 | 207 | * |
9e2eca0f | 208 | * @param boolean $force whether to force loading from DB even if it was already loaded |
fe817d87 | 209 | * @return stdClass|false definition data or false if the form is not defined yet |
3e43eff5 | 210 | */ |
9e2eca0f | 211 | public function get_definition($force = false) { |
5060997b | 212 | if ($this->definition === false || $force) { |
fe817d87 DM |
213 | $this->load_definition(); |
214 | } | |
215 | return $this->definition; | |
3e43eff5 DM |
216 | } |
217 | ||
fde33804 DM |
218 | /** |
219 | * Returns the form definition suitable for cloning into another area | |
220 | * | |
221 | * @param gradingform_controller $target the controller of the new copy | |
222 | * @return stdClass definition structure to pass to the target's {@link update_definition()} | |
223 | */ | |
224 | public function get_definition_copy(gradingform_controller $target) { | |
225 | ||
226 | if (get_class($this) != get_class($target)) { | |
227 | throw new coding_exception('The source and copy controller mismatch'); | |
228 | } | |
229 | ||
230 | if ($target->is_form_defined()) { | |
231 | throw new coding_exception('The target controller already contains a form definition'); | |
232 | } | |
233 | ||
234 | $old = $this->get_definition(); | |
235 | // keep our id | |
236 | $new = new stdClass(); | |
237 | $new->copiedfromid = $old->id; | |
238 | $new->name = $old->name; | |
239 | // once we support files embedded into the description, we will want to | |
240 | // relink them into the new file area here (that is why we accept $target) | |
241 | $new->description = $old->description; | |
242 | $new->descriptionformat = $old->descriptionformat; | |
243 | $new->options = $old->options; | |
3599b113 | 244 | $new->status = $old->status; |
fde33804 DM |
245 | |
246 | return $new; | |
247 | } | |
248 | ||
3e43eff5 | 249 | /** |
fe817d87 | 250 | * Saves the defintion data into the database |
21d37aa6 | 251 | * |
fe817d87 DM |
252 | * The implementation in this base class stores the common data into the record |
253 | * into the {grading_definition} table. The plugins are likely to extend this | |
254 | * and save their data into own tables, too. | |
21d37aa6 | 255 | * |
fe817d87 DM |
256 | * @param stdClass $definition data containing values for the {grading_definition} table |
257 | * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user | |
21d37aa6 | 258 | */ |
3599b113 | 259 | public function update_definition(stdClass $definition, $usermodified = null) { |
21d37aa6 DM |
260 | global $DB, $USER; |
261 | ||
262 | if (is_null($usermodified)) { | |
263 | $usermodified = $USER->id; | |
264 | } | |
265 | ||
fe817d87 DM |
266 | if (!empty($this->definition->id)) { |
267 | // prepare a record to be updated | |
268 | $record = new stdClass(); | |
269 | // populate it with scalar values from the passed definition structure | |
270 | foreach ($definition as $prop => $val) { | |
271 | if (is_array($val) or is_object($val)) { | |
272 | // probably plugin's data | |
273 | continue; | |
274 | } | |
275 | $record->{$prop} = $val; | |
276 | } | |
277 | // make sure we do not override some crucial values by accident | |
278 | if (!empty($record->id) and $record->id != $this->definition->id) { | |
279 | throw new coding_exception('Attempting to update other definition record.'); | |
280 | } | |
281 | $record->id = $this->definition->id; | |
282 | unset($record->areaid); | |
283 | unset($record->method); | |
284 | unset($record->timecreated); | |
285 | // set the modification flags | |
286 | $record->timemodified = time(); | |
287 | $record->usermodified = $usermodified; | |
21d37aa6 | 288 | |
fe817d87 | 289 | $DB->update_record('grading_definitions', $record); |
21d37aa6 DM |
290 | |
291 | } else if ($this->definition === false) { | |
fe817d87 DM |
292 | // prepare a record to be inserted |
293 | $record = new stdClass(); | |
294 | // populate it with scalar values from the passed definition structure | |
295 | foreach ($definition as $prop => $val) { | |
296 | if (is_array($val) or is_object($val)) { | |
297 | // probably plugin's data | |
298 | continue; | |
299 | } | |
300 | $record->{$prop} = $val; | |
301 | } | |
302 | // make sure we do not override some crucial values by accident | |
303 | if (!empty($record->id)) { | |
304 | throw new coding_exception('Attempting to create a new record while there is already one existing.'); | |
305 | } | |
306 | unset($record->id); | |
307 | $record->areaid = $this->areaid; | |
308 | $record->method = $this->get_method_name(); | |
309 | $record->timecreated = time(); | |
310 | $record->usercreated = $usermodified; | |
ac16cedf DM |
311 | $record->timemodified = $record->timecreated; |
312 | $record->usermodified = $record->usercreated; | |
3599b113 | 313 | if (empty($record->status)) { |
7622ae95 DM |
314 | $record->status = self::DEFINITION_STATUS_DRAFT; |
315 | } | |
3599b113 MG |
316 | if (empty($record->descriptionformat)) { |
317 | $record->descriptionformat = FORMAT_MOODLE; // field can not be empty | |
318 | } | |
21d37aa6 | 319 | |
ac16cedf | 320 | $DB->insert_record('grading_definitions', $record); |
21d37aa6 DM |
321 | |
322 | } else { | |
fe817d87 | 323 | throw new coding_exception('Unknown status of the cached definition record.'); |
21d37aa6 DM |
324 | } |
325 | } | |
326 | ||
3599b113 MG |
327 | /** |
328 | * Formats the definition description for display on page | |
329 | * | |
330 | * @return string | |
331 | */ | |
332 | public function get_formatted_description() { | |
333 | if (!isset($this->definition->description)) { | |
334 | return ''; | |
335 | } | |
336 | return format_text($this->definition->description, $this->definition->descriptionformat); | |
337 | } | |
338 | ||
21d37aa6 | 339 | /** |
0136124e MG |
340 | * Returns the current instance (either with status ACTIVE or NEEDUPDATE) for this definition for the |
341 | * specified $raterid and $itemid (if multiple raters are allowed, or only for $itemid otherwise). | |
21d37aa6 | 342 | * |
21d37aa6 DM |
343 | * @param int $raterid |
344 | * @param int $itemid | |
36937f02 MG |
345 | * @param boolean $idonly |
346 | * @return mixed if $idonly=true returns id of the found instance, otherwise returns the instance object | |
21d37aa6 | 347 | */ |
36937f02 | 348 | public function get_current_instance($raterid, $itemid, $idonly = false) { |
21d37aa6 | 349 | global $DB; |
0136124e | 350 | $params = array( |
36937f02 MG |
351 | 'formid' => $this->definition->id, |
352 | 'itemid' => $itemid, | |
0136124e MG |
353 | 'status1' => gradingform_instance::INSTANCE_STATUS_ACTIVE, |
354 | 'status2' => gradingform_instance::INSTANCE_STATUS_NEEDUPDATE); | |
355 | $select = 'formid=:formid and itemid=:itemid and (status=:status1 or status=:status2)'; | |
36937f02 | 356 | if (false /* TODO $manager->allow_multiple_raters() */) { |
0136124e MG |
357 | $select .= ' and raterid=:raterid'; |
358 | $params['raterid'] = $raterid; | |
21d37aa6 | 359 | } |
36937f02 | 360 | if ($idonly) { |
0136124e | 361 | if ($current = $DB->get_record_select('grading_instances', $select, $params, 'id', IGNORE_MISSING)) { |
36937f02 MG |
362 | return $current->id; |
363 | } | |
21d37aa6 | 364 | } else { |
0136124e | 365 | if ($current = $DB->get_record_select('grading_instances', $select, $params, '*', IGNORE_MISSING)) { |
36937f02 MG |
366 | return $this->get_instance($current); |
367 | } | |
21d37aa6 | 368 | } |
36937f02 | 369 | return null; |
21d37aa6 DM |
370 | } |
371 | ||
18e6298c | 372 | /** |
0136124e MG |
373 | * Returns list of ACTIVE instances for the specified $itemid |
374 | * (intentionally does not return instances with status NEEDUPDATE) | |
36937f02 MG |
375 | * |
376 | * @param int $itemid | |
377 | * @return array of gradingform_instance objects | |
18e6298c | 378 | */ |
0136124e | 379 | public function get_active_instances($itemid) { |
36937f02 MG |
380 | global $DB; |
381 | $conditions = array('formid' => $this->definition->id, | |
382 | 'itemid' => $itemid, | |
383 | 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); | |
384 | $records = $DB->get_recordset('grading_instances', $conditions); | |
385 | $rv = array(); | |
386 | foreach ($records as $record) { | |
387 | $rv[] = $this->get_instance($record); | |
388 | } | |
389 | return $rv; | |
390 | } | |
6798c63e | 391 | |
0136124e MG |
392 | /** |
393 | * Returns true if there are already people who has been graded on this definition. | |
394 | * In this case plugins may restrict changes of the grading definition | |
395 | * | |
396 | * @return boolean | |
397 | */ | |
398 | public function has_active_instances() { | |
399 | global $DB; | |
2d41a911 MG |
400 | if (empty($this->definition->id)) { |
401 | return false; | |
402 | } | |
0136124e MG |
403 | if ($this->hasactiveinstances === null) { |
404 | $conditions = array('formid' => $this->definition->id, | |
405 | 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); | |
406 | $this->hasactiveinstances = $DB->record_exists('grading_instances', $conditions); | |
407 | } | |
408 | return $this->hasactiveinstances; | |
409 | } | |
410 | ||
6798c63e | 411 | /** |
36937f02 | 412 | * Returns the object of type gradingform_XXX_instance (where XXX is the plugin method name) |
6798c63e | 413 | * |
36937f02 MG |
414 | * @param mixed $instance id or row from grading_isntances table |
415 | * @return gradingform_instance | |
6798c63e | 416 | */ |
36937f02 MG |
417 | protected function get_instance($instance) { |
418 | global $DB; | |
419 | if (is_scalar($instance)) { | |
420 | // instance id is passed as parameter | |
421 | $instance = $DB->get_record('grading_instances', array('id' => $instance), '*', MUST_EXIST); | |
422 | } | |
423 | if ($instance) { | |
424 | $class = 'gradingform_'. $this->get_method_name(). '_instance'; | |
425 | return new $class($this, $instance); | |
426 | } | |
427 | return null; | |
6798c63e MG |
428 | } |
429 | ||
430 | /** | |
36937f02 MG |
431 | * This function is invoked when user (teacher) starts grading. |
432 | * It creates and returns copy of the current ACTIVE instance if it exists. If this is the | |
433 | * first grading attempt, a new instance is created. | |
434 | * The status of the returned instance is INCOMPLETE | |
6798c63e | 435 | * |
36937f02 MG |
436 | * @param int $raterid |
437 | * @param int $itemid | |
438 | * @return gradingform_instance | |
6798c63e | 439 | */ |
fc05f222 | 440 | public function create_instance($raterid, $itemid = null) { |
6f19a2a9 | 441 | |
36937f02 | 442 | // first find if there is already an active instance for this itemid |
fc05f222 | 443 | if ($itemid && $current = $this->get_current_instance($raterid, $itemid)) { |
36937f02 MG |
444 | return $this->get_instance($current->copy($raterid, $itemid)); |
445 | } else { | |
446 | $class = 'gradingform_'. $this->get_method_name(). '_instance'; | |
447 | return $this->get_instance($class::create_new($this->definition->id, $raterid, $itemid)); | |
448 | } | |
6798c63e | 449 | } |
18e6298c | 450 | |
13926e73 MG |
451 | /** |
452 | * If instanceid is specified and grading instance exists and it is created by this rater for | |
453 | * this item, this instance is returned. | |
454 | * Otherwise new instance is created for the specified rater and itemid | |
455 | * | |
456 | * @param int $instanceid | |
457 | * @param int $raterid | |
458 | * @param int $itemid | |
459 | * @return gradingform_instance | |
460 | */ | |
461 | public function get_or_create_instance($instanceid, $raterid, $itemid) { | |
462 | global $DB; | |
463 | if ($instanceid && | |
464 | $instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) { | |
465 | return $this->get_instance($instance); | |
466 | } | |
467 | return $this->create_instance($raterid, $itemid); | |
468 | } | |
469 | ||
ab156741 | 470 | /** |
6832a102 | 471 | * Returns the HTML code displaying the preview of the grading form |
ab156741 | 472 | * |
20836db9 | 473 | * Plugins are forced to override this. Ideally they should delegate |
6832a102 DM |
474 | * the task to their own renderer. |
475 | * | |
476 | * @param moodle_page $page the target page | |
477 | * @return string | |
ab156741 | 478 | */ |
20836db9 | 479 | abstract public function render_preview(moodle_page $page); |
ab156741 | 480 | |
671ec8f5 DM |
481 | /** |
482 | * Deletes the form definition and all the associated data | |
483 | * | |
484 | * @see delete_plugin_definition() | |
485 | * @return void | |
486 | */ | |
487 | public function delete_definition() { | |
488 | global $DB; | |
489 | ||
490 | if (!$this->is_form_defined()) { | |
491 | // nothing to do | |
492 | return; | |
493 | } | |
fe817d87 | 494 | |
671ec8f5 DM |
495 | // firstly, let the plugin delete everything from their own tables |
496 | $this->delete_plugin_definition(); | |
497 | // then, delete all instances left | |
498 | $DB->delete_records('grading_instances', array('formid' => $this->definition->id)); | |
499 | // finally, delete the main definition record | |
500 | $DB->delete_records('grading_definitions', array('id' => $this->definition->id)); | |
501 | ||
502 | $this->definition = false; | |
503 | } | |
504 | ||
20836db9 DM |
505 | /** |
506 | * Prepare the part of the search query to append to the FROM statement | |
507 | * | |
508 | * @param string $gdid the alias of grading_definitions.id column used by the caller | |
509 | * @return string | |
510 | */ | |
511 | public static function sql_search_from_tables($gdid) { | |
512 | return ''; | |
513 | } | |
514 | ||
515 | /** | |
516 | * Prepare the parts of the SQL WHERE statement to search for the given token | |
517 | * | |
518 | * The returned array cosists of the list of SQL comparions and the list of | |
519 | * respective parameters for the comparisons. The returned chunks will be joined | |
520 | * with other conditions using the OR operator. | |
521 | * | |
522 | * @param string $token token to search for | |
523 | * @return array | |
524 | */ | |
525 | public static function sql_search_where($token) { | |
20836db9 DM |
526 | |
527 | $subsql = array(); | |
528 | $params = array(); | |
529 | ||
530 | return array($subsql, $params); | |
531 | } | |
532 | ||
671ec8f5 | 533 | //////////////////////////////////////////////////////////////////////////// |
fe817d87 | 534 | |
21d37aa6 DM |
535 | /** |
536 | * Loads the form definition if it exists | |
3e43eff5 | 537 | * |
21d37aa6 | 538 | * The default implementation just tries to load the record from the {grading_definitions} |
3e43eff5 DM |
539 | * table. The plugins are likely to override this with a more complex query that loads |
540 | * all required data at once. | |
541 | */ | |
542 | protected function load_definition() { | |
543 | global $DB; | |
3e43eff5 DM |
544 | $this->definition = $DB->get_record('grading_definitions', array( |
545 | 'areaid' => $this->areaid, | |
546 | 'method' => $this->get_method_name()), '*', IGNORE_MISSING); | |
547 | } | |
21d37aa6 | 548 | |
671ec8f5 DM |
549 | /** |
550 | * Deletes all plugin data associated with the given form definiton | |
551 | * | |
552 | * @see delete_definition() | |
553 | */ | |
554 | abstract protected function delete_plugin_definition(); | |
555 | ||
21d37aa6 | 556 | /** |
fe817d87 DM |
557 | * @return string the name of the grading method plugin, eg 'rubric' |
558 | * @see PARAM_PLUGIN | |
21d37aa6 | 559 | */ |
fe817d87 DM |
560 | protected function get_method_name() { |
561 | if (preg_match('/^gradingform_([a-z][a-z0-9_]*[a-z0-9])_controller$/', get_class($this), $matches)) { | |
562 | return $matches[1]; | |
563 | } else { | |
564 | throw new coding_exception('Invalid class name'); | |
565 | } | |
21d37aa6 | 566 | } |
36937f02 MG |
567 | |
568 | /** | |
569 | * Returns html code to be included in student's feedback. | |
570 | * | |
571 | * @param moodle_page $page | |
572 | * @param int $itemid | |
9e2eca0f MG |
573 | * @param array $grading_info result of function grade_get_grades if plugin want to use some of their info |
574 | * @param string $defaultcontent default string to be returned if no active grading is found or for some reason can not be shown to a user | |
0136124e | 575 | * @param boolean $cangrade whether current user has capability to grade in this context |
36937f02 MG |
576 | * @return string |
577 | */ | |
0136124e | 578 | public function render_grade($page, $itemid, $grading_info, $defaultcontent, $cangrade) { |
36937f02 MG |
579 | return $defaultcontent; |
580 | } | |
9e2eca0f MG |
581 | |
582 | /** | |
583 | * Sets the range of grades used in this area. This is usually either range like 0-100 | |
584 | * or the scale where keys start from 1. Typical use: | |
585 | * $controller->set_grade_range(make_grades_menu($gradingtype)); | |
586 | */ | |
587 | public final function set_grade_range(array $graderange) { | |
588 | $this->graderange = $graderange; | |
589 | } | |
590 | ||
591 | /** | |
592 | * Returns the range of grades used in this area | |
593 | * @return array | |
594 | */ | |
595 | public final function get_grade_range() { | |
596 | if (empty($this->graderange)) { | |
597 | return array(); | |
598 | } | |
599 | return $this->graderange; | |
600 | } | |
21d37aa6 | 601 | } |
36937f02 MG |
602 | |
603 | /** | |
604 | * Class to manage one grading instance. Stores information and performs actions like | |
605 | * update, copy, validate, submit, etc. | |
606 | * | |
607 | * @copyright 2011 Marina Glancy | |
608 | */ | |
609 | abstract class gradingform_instance { | |
610 | const INSTANCE_STATUS_ACTIVE = 1; | |
0136124e | 611 | const INSTANCE_STATUS_NEEDUPDATE = 2; |
36937f02 MG |
612 | const INSTANCE_STATUS_INCOMPLETE = 0; |
613 | const INSTANCE_STATUS_ARCHIVE = 3; | |
614 | ||
615 | /** @var stdClass record from table grading_instances */ | |
616 | protected $data; | |
617 | /** @var gradingform_controller link to the corresponding controller */ | |
618 | protected $controller; | |
619 | ||
620 | /** | |
621 | * Creates an instance | |
622 | * | |
623 | * @param gradingform_controller $controller | |
624 | * @param stdClass $data | |
625 | */ | |
626 | public function __construct($controller, $data) { | |
627 | $this->data = (object)$data; | |
628 | $this->controller = $controller; | |
629 | } | |
630 | ||
631 | /** | |
632 | * Creates a new empty instance in DB and mark its status as INCOMPLETE | |
633 | * | |
634 | * @param int $formid | |
635 | * @param int $raterid | |
636 | * @param int $itemid | |
637 | * @return int id of the created instance | |
638 | */ | |
639 | public static function create_new($formid, $raterid, $itemid) { | |
640 | global $DB; | |
641 | $instance = new stdClass(); | |
642 | $instance->formid = $formid; | |
643 | $instance->raterid = $raterid; | |
644 | $instance->itemid = $itemid; | |
645 | $instance->status = self::INSTANCE_STATUS_INCOMPLETE; | |
646 | $instance->timemodified = time(); | |
647 | $instance->feedbackformat = FORMAT_MOODLE; | |
648 | $instanceid = $DB->insert_record('grading_instances', $instance); | |
649 | return $instanceid; | |
650 | } | |
651 | ||
652 | /** | |
653 | * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with | |
654 | * the specified values) | |
655 | * Plugins may want to override this function to copy data from additional tables as well | |
656 | * | |
657 | * @param int $raterid value for raterid in the duplicate | |
658 | * @param int $itemid value for itemid in the duplicate | |
659 | * @return int id of the new instance | |
660 | */ | |
661 | public function copy($raterid, $itemid) { | |
662 | global $DB; | |
663 | $data = (array)$this->data; // Cast to array to make a copy | |
664 | unset($data['id']); | |
665 | $data['raterid'] = $raterid; | |
666 | $data['itemid'] = $itemid; | |
667 | $data['timemodified'] = time(); | |
668 | $data['status'] = self::INSTANCE_STATUS_INCOMPLETE; | |
669 | $instanceid = $DB->insert_record('grading_instances', $data); | |
670 | return $instanceid; | |
671 | } | |
672 | ||
0136124e MG |
673 | /** |
674 | * Returns the current (active or needupdate) instance for the same raterid and itemid as this | |
675 | * instance. This function is useful to find the status of the currently modified instance | |
676 | * | |
677 | * @return gradingform_instance | |
678 | */ | |
679 | public function get_current_instance() { | |
680 | if ($this->get_status() == self::INSTANCE_STATUS_ACTIVE || $this->get_status() == self::INSTANCE_STATUS_NEEDUPDATE) { | |
681 | return $this; | |
682 | } | |
683 | return $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid); | |
684 | } | |
685 | ||
36937f02 MG |
686 | /** |
687 | * Returns the controller | |
688 | * | |
689 | * @return gradingform_controller | |
690 | */ | |
691 | public function get_controller() { | |
692 | return $this->controller; | |
693 | } | |
694 | ||
188e840b MG |
695 | /** |
696 | * Returns the specified element from object $this->data | |
697 | * | |
698 | * @param string $key | |
699 | * @return mixed | |
700 | */ | |
701 | public function get_data($key) { | |
702 | if (isset($this->data->$key)) { | |
703 | return $this->data->$key; | |
704 | } | |
705 | return null; | |
706 | } | |
707 | ||
36937f02 MG |
708 | /** |
709 | * Returns instance id | |
710 | * | |
711 | * @return int | |
712 | */ | |
713 | public function get_id() { | |
188e840b | 714 | return $this->get_data('id'); |
36937f02 MG |
715 | } |
716 | ||
0136124e MG |
717 | /** |
718 | * Returns instance status | |
719 | * | |
720 | * @return int | |
721 | */ | |
722 | public function get_status() { | |
188e840b | 723 | return $this->get_data('status'); |
0136124e MG |
724 | } |
725 | ||
36937f02 MG |
726 | /** |
727 | * Marks the instance as ACTIVE and current active instance (if exists) as ARCHIVE | |
728 | */ | |
729 | protected function make_active() { | |
730 | global $DB; | |
731 | if ($this->data->status == self::INSTANCE_STATUS_ACTIVE) { | |
732 | // already active | |
733 | return; | |
734 | } | |
fc05f222 MG |
735 | if (empty($this->data->itemid)) { |
736 | throw new coding_exception('You cannot mark active the grading instance without itemid'); | |
737 | } | |
36937f02 | 738 | $currentid = $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid, true); |
0136124e MG |
739 | if ($currentid && $currentid != $this->get_id()) { |
740 | $DB->update_record('grading_instances', array('id' => $currentid, 'status' => self::INSTANCE_STATUS_ARCHIVE)); | |
36937f02 | 741 | } |
0136124e | 742 | $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE)); |
36937f02 MG |
743 | $this->data->status = self::INSTANCE_STATUS_ACTIVE; |
744 | } | |
745 | ||
746 | /** | |
747 | * Deletes this (INCOMPLETE) instance from database. This function is invoked on cancelling the | |
748 | * grading form and/or during cron cleanup. | |
749 | * Plugins using additional tables must override this method to remove additional data. | |
750 | * Note that if the teacher just closes the window or presses 'Back' button of the browser, | |
751 | * this function is not invoked. | |
752 | */ | |
753 | public function cancel() { | |
754 | global $DB; | |
755 | // TODO what if we happen delete the ACTIVE instance, shall we rollback to the last ARCHIVE? or throw an exception? | |
756 | // TODO create cleanup cron | |
757 | $DB->delete_records('grading_instances', array('id' => $this->get_id())); | |
758 | } | |
759 | ||
760 | /** | |
761 | * Updates the instance with the data received from grading form. This function may be | |
762 | * called via AJAX when grading is not yet completed, so it does not change the | |
763 | * status of the instance. | |
fc05f222 MG |
764 | * |
765 | * @param array $elementvalue | |
36937f02 MG |
766 | */ |
767 | public function update($elementvalue) { | |
fc05f222 MG |
768 | global $DB; |
769 | $newdata = new stdClass(); | |
770 | $newdata->id = $this->get_id(); | |
771 | $newdata->timemodified = time(); | |
772 | if (isset($elementvalue['itemid']) && $elementvalue['itemid'] != $this->data->itemid) { | |
773 | $newdata->itemid = $elementvalue['itemid']; | |
774 | } | |
775 | // TODO also update: rawgrade, feedback, feedbackformat | |
776 | $DB->update_record('grading_instances', $newdata); | |
777 | foreach ($newdata as $key => $value) { | |
778 | $this->data->$key = $value; | |
779 | } | |
36937f02 MG |
780 | } |
781 | ||
782 | /** | |
783 | * Calculates the grade to be pushed to the gradebook | |
9e2eca0f MG |
784 | * |
785 | * @return int the valid grade from $this->get_controller()->get_grade_range() | |
36937f02 MG |
786 | */ |
787 | abstract public function get_grade(); | |
788 | ||
789 | /** | |
790 | * Called when teacher submits the grading form: | |
fc05f222 MG |
791 | * updates the instance in DB, marks it as ACTIVE and returns the grade to be pushed to the gradebook. |
792 | * $itemid must be specified here (it was not required when the instance was | |
793 | * created, because it might not existed in draft) | |
794 | * | |
795 | * @param array $elementvalue | |
796 | * @param int $itemid | |
36937f02 MG |
797 | * @return int the grade on 0-100 scale |
798 | */ | |
fc05f222 MG |
799 | public function submit_and_get_grade($elementvalue, $itemid) { |
800 | $elementvalue['itemid'] = $itemid; | |
36937f02 MG |
801 | $this->update($elementvalue); |
802 | $this->make_active(); | |
803 | return $this->get_grade(); | |
804 | } | |
805 | ||
806 | ||
807 | /** | |
808 | * Returns html for form element of type 'grading'. If there is a form input element | |
809 | * it must have the name $gradingformelement->getName(). | |
810 | * If there are more than one input elements they MUST be elements of array with | |
811 | * name $gradingformelement->getName(). | |
812 | * Example: {NAME}[myelement1], {NAME}[myelement2][sub1], {NAME}[myelement2][sub2], etc. | |
9e2eca0f | 813 | * ( {NAME} is a shortcut for $gradingformelement->getName() ) |
36937f02 MG |
814 | * After submitting the form the value of $_POST[{NAME}] is passed to the functions |
815 | * validate_grading_element() and submit_and_get_grade() | |
816 | * | |
817 | * Plugins may use $gradingformelement->getValue() to get the value passed on previous | |
9e2eca0f | 818 | * form submit |
36937f02 MG |
819 | * |
820 | * When forming html it is a plugin's responsibility to analyze flags | |
821 | * $gradingformelement->_flagFrozen and $gradingformelement->_persistantFreeze: | |
822 | * | |
823 | * (_flagFrozen == false) => form element is editable | |
824 | * | |
825 | * (_flagFrozen == false && _persistantFreeze == true) => form element is not editable | |
826 | * but all values are passed as hidden elements | |
827 | * | |
828 | * (_flagFrozen == false && _persistantFreeze == false) => form element is not editable | |
829 | * and no values are passed as hidden elements | |
830 | * | |
831 | * Plugins are welcome to use AJAX in the form element. But it is strongly recommended | |
832 | * that the grading only becomes active when teacher presses 'Submit' button (the | |
833 | * method submit_and_get_grade() is invoked) | |
834 | * | |
835 | * Also client-side JS validation may be implemented here | |
836 | * | |
837 | * @see MoodleQuickForm_grading in lib/form/grading.php | |
838 | * | |
839 | * @param moodle_page $page | |
840 | * @param MoodleQuickForm_grading $gradingformelement | |
841 | * @return string | |
842 | */ | |
843 | abstract function render_grading_element($page, $gradingformelement); | |
844 | ||
845 | /** | |
846 | * Server-side validation of the data received from grading form. | |
847 | * | |
848 | * @param mixed $elementvalue is the scalar or array received in $_POST | |
849 | * @return boolean true if the form data is validated and contains no errors | |
850 | */ | |
851 | public function validate_grading_element($elementvalue) { | |
852 | return true; | |
853 | } | |
854 | ||
855 | /** | |
856 | * Returns the error message displayed if validation failed. | |
857 | * If plugin wants to display custom message, the empty string should be returned here | |
858 | * and the custom message should be output in render_grading_element() | |
859 | * | |
860 | * @see validate_grading_element() | |
861 | * @return string | |
862 | */ | |
863 | public function default_validation_error_message() { | |
864 | return ''; | |
865 | } | |
866 | } |