MDL-55584 core_grades: Convert part of set-up screen to template
[moodle.git] / grade / grading / lib.php
CommitLineData
4333580e 1<?php
4333580e
DM
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 * Advanced grading methods support
19 *
d22e9e32 20 * @package core_grading
4333580e
DM
21 * @copyright 2011 David Mudrak <david@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27/**
28 * Factory method returning an instance of the grading manager
29 *
217116d4
DM
30 * There are basically ways how to use this factory method. If the area record
31 * id is known to the caller, get the manager for that area by providing just
32 * the id. If the area record id is not know, the context, component and area name
33 * can be provided. Note that null values are allowed in the second case as the context,
34 * component and the area name can be set explicitly later.
35 *
d22e9e32 36 * @category grading
217116d4 37 * @example $manager = get_grading_manager($areaid);
0601e0ee 38 * @example $manager = get_grading_manager(context_system::instance());
217116d4 39 * @example $manager = get_grading_manager($context, 'mod_assignment', 'submission');
d22e9e32 40 * @param stdClass|int|null $context_or_areaid if $areaid is passed, no other parameter is needed
217116d4
DM
41 * @param string|null $component the frankenstyle name of the component
42 * @param string|null $area the name of the gradable area
4333580e
DM
43 * @return grading_manager
44 */
fb13a148
DM
45function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
46 global $DB;
4333580e
DM
47
48 $manager = new grading_manager();
49
fb13a148
DM
50 if (is_object($context_or_areaid)) {
51 $context = $context_or_areaid;
52 } else {
53 $context = null;
54
55 if (is_numeric($context_or_areaid)) {
56 $manager->load($context_or_areaid);
57 return $manager;
58 }
59 }
60
4333580e
DM
61 if (!is_null($context)) {
62 $manager->set_context($context);
63 }
64
65 if (!is_null($component)) {
66 $manager->set_component($component);
67 }
68
8a4acb3a
DM
69 if (!is_null($area)) {
70 $manager->set_area($area);
4333580e
DM
71 }
72
73 return $manager;
74}
75
76/**
77 * General class providing access to common grading features
78 *
9b8550f8
DM
79 * Grading manager provides access to the particular grading method controller
80 * in that area.
81 *
4333580e
DM
82 * Fully initialized instance of the grading manager operates over a single
83 * gradable area. It is possible to work with a partially initialized manager
8a4acb3a
DM
84 * that knows just context and component without known area, for example.
85 * It is also possible to change context, component and area of an existing
4333580e 86 * manager. Such pattern is used when copying form definitions, for example.
d22e9e32 87 *
5dc1ca87 88 * @package core_grading
d22e9e32
MG
89 * @copyright 2011 David Mudrak <david@moodle.com>
90 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
91 * @category grading
4333580e
DM
92 */
93class grading_manager {
94
95 /** @var stdClass the context */
96 protected $context;
97
98 /** @var string the frankenstyle name of the component */
99 protected $component;
100
101 /** @var string the name of the gradable area */
8a4acb3a 102 protected $area;
4333580e 103
9b8550f8
DM
104 /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
105 private $areacache = null;
106
21d37aa6 107 /**
d22e9e32
MG
108 * Returns grading manager context
109 *
21d37aa6
DM
110 * @return stdClass grading manager context
111 */
112 public function get_context() {
113 return $this->context;
114 }
115
4333580e
DM
116 /**
117 * Sets the context the manager operates on
118 *
119 * @param stdClass $context
120 */
121 public function set_context(stdClass $context) {
9b8550f8 122 $this->areacache = null;
4333580e
DM
123 $this->context = $context;
124 }
125
21d37aa6 126 /**
d22e9e32
MG
127 * Returns grading manager component
128 *
21d37aa6
DM
129 * @return string grading manager component
130 */
131 public function get_component() {
132 return $this->component;
133 }
134
4333580e
DM
135 /**
136 * Sets the component the manager operates on
137 *
138 * @param string $component the frankenstyle name of the component
139 */
140 public function set_component($component) {
9b8550f8 141 $this->areacache = null;
56da374e 142 list($type, $name) = core_component::normalize_component($component);
9b8550f8 143 $this->component = $type.'_'.$name;
4333580e
DM
144 }
145
21d37aa6 146 /**
d22e9e32
MG
147 * Returns grading manager area name
148 *
21d37aa6
DM
149 * @return string grading manager area name
150 */
151 public function get_area() {
152 return $this->area;
153 }
154
4333580e 155 /**
8a4acb3a 156 * Sets the area the manager operates on
4333580e 157 *
8a4acb3a 158 * @param string $area the name of the gradable area
4333580e 159 */
8a4acb3a 160 public function set_area($area) {
9b8550f8 161 $this->areacache = null;
8a4acb3a 162 $this->area = $area;
4333580e
DM
163 }
164
6832a102
DM
165 /**
166 * Returns a text describing the context and the component
167 *
168 * At the moment this works for gradable areas in course modules. In the future, this
169 * method should be improved so it works for other contexts (blocks, gradebook items etc)
170 * or subplugins.
171 *
172 * @return string
173 */
174 public function get_component_title() {
175
176 $this->ensure_isset(array('context', 'component'));
6832a102 177
fde33804
DM
178 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
179 if ($this->get_component() == 'core_grading') {
180 $title = ''; // we are in the bank UI
181 } else {
182 throw new coding_exception('Unsupported component at the system context');
183 }
184
185 } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
186 list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
187
278b8d18 188 if (strval($cm->name) !== '') {
fde33804
DM
189 $title = $cm->name;
190 } else {
191 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
192 $title = $this->get_component();
193 }
194
6832a102 195 } else {
fde33804 196 throw new coding_exception('Unsupported gradable area context level');
6832a102
DM
197 }
198
199 return $title;
200 }
201
202 /**
203 * Returns the localized title of the currently set area
204 *
205 * @return string
206 */
207 public function get_area_title() {
208
fde33804
DM
209 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
210 return '';
211
212 } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
213 $this->ensure_isset(array('context', 'component', 'area'));
214 $areas = $this->get_available_areas();
215 if (array_key_exists($this->get_area(), $areas)) {
216 return $areas[$this->get_area()];
217 } else {
218 debugging('Unknown area!');
219 return '???';
220 }
6832a102 221
fde33804
DM
222 } else {
223 throw new coding_exception('Unsupported context level');
224 }
6832a102
DM
225 }
226
fb13a148
DM
227 /**
228 * Loads the gradable area info from the database
229 *
230 * @param int $areaid
231 */
232 public function load($areaid) {
233 global $DB;
234
235 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
d197ea43 236 $this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST);
fb13a148
DM
237 $this->component = $this->areacache->component;
238 $this->area = $this->areacache->areaname;
239 }
240
4333580e 241 /**
bb50c376
DM
242 * Returns the list of installed grading plugins together, optionally extended
243 * with a simple direct grading.
6c9e506c
DM
244 *
245 * @param bool $includenone should the 'Simple direct grading' be included
4333580e
DM
246 * @return array of the (string)name => (string)localized title of the method
247 */
bb50c376 248 public static function available_methods($includenone = true) {
4333580e 249
6c9e506c
DM
250 if ($includenone) {
251 $list = array('' => get_string('gradingmethodnone', 'core_grading'));
252 } else {
253 $list = array();
254 }
255
bd3b3bba 256 foreach (core_component::get_plugin_list('gradingform') as $name => $location) {
6c9e506c
DM
257 $list[$name] = get_string('pluginname', 'gradingform_'.$name);
258 }
259
260 return $list;
4333580e
DM
261 }
262
bb50c376
DM
263 /**
264 * Returns the list of available grading methods in the given context
265 *
266 * Currently this is just a static list obtained from {@link self::available_methods()}.
267 * In the future, the list of available methods may be controlled per-context.
268 *
269 * Requires the context property to be set in advance.
270 *
271 * @param bool $includenone should the 'Simple direct grading' be included
272 * @return array of the (string)name => (string)localized title of the method
273 */
274 public function get_available_methods($includenone = true) {
275 $this->ensure_isset(array('context'));
276 return self::available_methods($includenone);
277 }
278
279 /**
280 * Returns the list of gradable areas provided by the given component
281 *
282 * This performs a callback to the library of the relevant plugin to obtain
283 * the list of supported areas.
284 *
285 * @param string $component normalized component name
286 * @return array of (string)areacode => (string)localized title of the area
287 */
288 public static function available_areas($component) {
289 global $CFG;
290
56da374e 291 list($plugintype, $pluginname) = core_component::normalize_component($component);
bb50c376
DM
292
293 if ($component === 'core_grading') {
294 return array();
295
296 } else if ($plugintype === 'mod') {
297 return plugin_callback('mod', $pluginname, 'grading', 'areas_list', null, array());
298
299 } else {
300 throw new coding_exception('Unsupported area location');
301 }
302 }
303
304
4333580e
DM
305 /**
306 * Returns the list of gradable areas in the given context and component
307 *
308 * This performs a callback to the library of the relevant plugin to obtain
309 * the list of supported areas.
310 * @return array of (string)areacode => (string)localized title of the area
311 */
312 public function get_available_areas() {
313 global $CFG;
314
315 $this->ensure_isset(array('context', 'component'));
316
9e4eb676
DM
317 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
318 if ($this->get_component() !== 'core_grading') {
319 throw new coding_exception('Unsupported component at the system context');
320 } else {
321 return array();
322 }
323
bb50c376 324 } else if ($this->get_context()->contextlevel == CONTEXT_MODULE) {
9e4eb676 325 list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
bb50c376 326 return self::available_areas('mod_'.$cm->modname);
9e4eb676
DM
327
328 } else {
329 throw new coding_exception('Unsupported gradable area context level');
330 }
4333580e
DM
331 }
332
333 /**
8a4acb3a 334 * Returns the currently active grading method in the gradable area
4333580e 335 *
64402867 336 * @return string|null the name of the grading plugin of null if it has not been set
4333580e 337 */
8a4acb3a 338 public function get_active_method() {
64402867
DM
339 global $DB;
340
341 $this->ensure_isset(array('context', 'component', 'area'));
342
343 // get the current grading area record if it exists
9b8550f8
DM
344 if (is_null($this->areacache)) {
345 $this->areacache = $DB->get_record('grading_areas', array(
346 'contextid' => $this->context->id,
347 'component' => $this->component,
348 'areaname' => $this->area),
349 '*', IGNORE_MISSING);
350 }
64402867 351
9b8550f8 352 if ($this->areacache === false) {
64402867
DM
353 // no area record yet
354 return null;
355 }
356
9b8550f8 357 return $this->areacache->activemethod;
64402867
DM
358 }
359
360 /**
361 * Sets the currently active grading method in the gradable area
362 *
363 * @param string $method the method name, eg 'rubric' (must be available)
3bd217aa 364 * @return bool true if the method changed or was just set, false otherwise
64402867
DM
365 */
366 public function set_active_method($method) {
367 global $DB;
368
8a4acb3a 369 $this->ensure_isset(array('context', 'component', 'area'));
64402867 370
8cd65f16
DM
371 // make sure the passed method is empty or a valid plugin name
372 if (empty($method)) {
373 $method = null;
374 } else {
375 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
376 throw new moodle_exception('invalid_method_name', 'core_grading');
377 }
378 $available = $this->get_available_methods(false);
379 if (!array_key_exists($method, $available)) {
380 throw new moodle_exception('invalid_method_name', 'core_grading');
381 }
64402867
DM
382 }
383
384 // get the current grading area record if it exists
9b8550f8
DM
385 if (is_null($this->areacache)) {
386 $this->areacache = $DB->get_record('grading_areas', array(
387 'contextid' => $this->context->id,
388 'component' => $this->component,
389 'areaname' => $this->area),
390 '*', IGNORE_MISSING);
391 }
64402867 392
3bd217aa
DM
393 $methodchanged = false;
394
9b8550f8 395 if ($this->areacache === false) {
64402867
DM
396 // no area record yet, create one with the active method set
397 $area = array(
398 'contextid' => $this->context->id,
399 'component' => $this->component,
400 'areaname' => $this->area,
401 'activemethod' => $method);
402 $DB->insert_record('grading_areas', $area);
3bd217aa 403 $methodchanged = true;
64402867
DM
404
405 } else {
406 // update the existing record if needed
8cd65f16 407 if ($this->areacache->activemethod !== $method) {
9b8550f8 408 $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
3bd217aa 409 $methodchanged = true;
64402867
DM
410 }
411 }
9b8550f8
DM
412
413 $this->areacache = null;
3bd217aa
DM
414
415 return $methodchanged;
9b8550f8
DM
416 }
417
418 /**
419 * Extends the settings navigation with the grading settings
420 *
421 * This function is called when the context for the page is an activity module with the
422 * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms.
423 *
424 * @param settings_navigation $settingsnav {@link settings_navigation}
425 * @param navigation_node $modulenode {@link navigation_node}
426 */
427 public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
9b8550f8
DM
428
429 $this->ensure_isset(array('context', 'component'));
430
431 $areas = $this->get_available_areas();
432
433 if (empty($areas)) {
434 // no money, no funny
435 return;
8168299d 436
98a73bb7
DM
437 } else if (count($areas) == 1) {
438 // make just a single node for the management screen
439 $areatitle = reset($areas);
440 $areaname = key($areas);
9b8550f8
DM
441 $this->set_area($areaname);
442 $method = $this->get_active_method();
98a73bb7
DM
443 $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
444 $this->get_management_url(), settings_navigation::TYPE_CUSTOM);
445 if ($method) {
446 $controller = $this->get_controller($method);
447 $controller->extend_settings_navigation($settingsnav, $managementnode);
9b8550f8
DM
448 }
449
98a73bb7
DM
450 } else {
451 // make management screen node for each area
452 $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
453 null, settings_navigation::TYPE_CUSTOM);
454 foreach ($areas as $areaname => $areatitle) {
455 $this->set_area($areaname);
456 $method = $this->get_active_method();
457 $node = $managementnode->add($areatitle,
458 $this->get_management_url(), settings_navigation::TYPE_CUSTOM);
459 if ($method) {
460 $controller = $this->get_controller($method);
461 $controller->extend_settings_navigation($settingsnav, $node);
462 }
9b8550f8 463 }
9b8550f8
DM
464 }
465 }
466
577c8964
MG
467 /**
468 * Extends the module navigation with the advanced grading information
469 *
470 * This function is called when the context for the page is an activity module with the
471 * FEATURE_ADVANCED_GRADING.
472 *
473 * @param global_navigation $navigation
474 * @param navigation_node $modulenode
475 */
476 public function extend_navigation(global_navigation $navigation, navigation_node $modulenode=null) {
477 $this->ensure_isset(array('context', 'component'));
478
479 $areas = $this->get_available_areas();
480 foreach ($areas as $areaname => $areatitle) {
481 $this->set_area($areaname);
482 if ($controller = $this->get_active_controller()) {
483 $controller->extend_navigation($navigation, $modulenode);
484 }
485 }
486 }
487
9b8550f8
DM
488 /**
489 * Returns the given method's controller in the gradable area
490 *
491 * @param string $method the method name, eg 'rubric' (must be available)
492 * @return grading_controller
493 */
494 public function get_controller($method) {
776d75b9 495 global $CFG, $DB;
9b8550f8
DM
496
497 $this->ensure_isset(array('context', 'component', 'area'));
498
499 // make sure the passed method is a valid plugin name
500 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
501 throw new moodle_exception('invalid_method_name', 'core_grading');
502 }
503 $available = $this->get_available_methods(false);
504 if (!array_key_exists($method, $available)) {
505 throw new moodle_exception('invalid_method_name', 'core_grading');
506 }
507
508 // get the current grading area record if it exists
509 if (is_null($this->areacache)) {
510 $this->areacache = $DB->get_record('grading_areas', array(
511 'contextid' => $this->context->id,
512 'component' => $this->component,
513 'areaname' => $this->area),
514 '*', IGNORE_MISSING);
515 }
516
517 if ($this->areacache === false) {
518 // no area record yet, create one
519 $area = array(
520 'contextid' => $this->context->id,
521 'component' => $this->component,
522 'areaname' => $this->area);
523 $areaid = $DB->insert_record('grading_areas', $area);
524 // reload the cache
525 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
526 }
527
528 require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
3e43eff5 529 $classname = 'gradingform_'.$method.'_controller';
9b8550f8
DM
530
531 return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
4333580e
DM
532 }
533
18e6298c
MG
534 /**
535 * Returns the controller for the active method if it is available
03d448e5
DM
536 *
537 * @return null|grading_controller
18e6298c
MG
538 */
539 public function get_active_controller() {
540 if ($gradingmethod = $this->get_active_method()) {
541 $controller = $this->get_controller($gradingmethod);
542 if ($controller->is_form_available()) {
543 return $controller;
544 }
545 }
546 return null;
547 }
548
03d448e5
DM
549 /**
550 * Returns the URL of the grading area management page
551 *
552 * @param moodle_url $returnurl optional URL of the page where the user should be sent back to
553 * @return moodle_url
554 */
98a73bb7 555 public function get_management_url(moodle_url $returnurl = null) {
03d448e5
DM
556
557 $this->ensure_isset(array('context', 'component'));
558
559 if ($this->areacache) {
560 $params = array('areaid' => $this->areacache->id);
561 } else {
562 $params = array('contextid' => $this->context->id, 'component' => $this->component);
98a73bb7
DM
563 if ($this->area) {
564 $params['area'] = $this->area;
6832a102 565 }
03d448e5
DM
566 }
567
568 if (!is_null($returnurl)) {
569 $params['returnurl'] = $returnurl->out(false);
570 }
571
6832a102 572 return new moodle_url('/grade/grading/manage.php', $params);
03d448e5
DM
573 }
574
fde33804
DM
575 /**
576 * Creates a new shared area to hold a grading form template
577 *
578 * Shared area are implemented as virtual gradable areas at the system level context
579 * with the component set to core_grading and unique random area name.
580 *
581 * @param string $method the name of the plugin we create the area for
582 * @return int the new area id
583 */
584 public function create_shared_area($method) {
585 global $DB;
586
587 // generate some unique random name for the new area
588 $name = $method . '_' . sha1(rand().uniqid($method, true));
589 // create new area record
590 $area = array(
dd736a87 591 'contextid' => context_system::instance()->id,
fde33804
DM
592 'component' => 'core_grading',
593 'areaname' => $name,
594 'activemethod' => $method);
595 return $DB->insert_record('grading_areas', $area);
596 }
597
9e32368b
DM
598 /**
599 * Removes all data associated with the given context
600 *
601 * This is called by {@link context::delete_content()}
602 *
603 * @param int $contextid context id
604 */
605 public static function delete_all_for_context($contextid) {
606 global $DB;
607
608 $areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid));
609 $methods = array_keys(self::available_methods(false));
610
611 foreach($areaids as $areaid) {
612 $manager = get_grading_manager($areaid);
613 foreach ($methods as $method) {
614 $controller = $manager->get_controller($method);
615 $controller->delete_definition();
616 }
617 }
618
619 $DB->delete_records_list('grading_areas', 'id', $areaids);
620 }
621
20836db9
DM
622 /**
623 * Helper method to tokenize the given string
624 *
625 * Splits the given string into smaller strings. This is a helper method for
626 * full text searching in grading forms. If the given string is surrounded with
627 * double quotes, the resulting array consists of a single item containing the
628 * quoted content.
629 *
630 * Otherwise, string like 'grammar, english language' would be tokenized into
631 * the three tokens 'grammar', 'english', 'language'.
632 *
633 * One-letter tokens like are dropped in non-phrase mode. Repeated tokens are
634 * returned just once.
635 *
636 * @param string $needle
637 * @return array
638 */
639 public static function tokenize($needle) {
640
641 // check if we are searching for the exact phrase
642 if (preg_match('/^[\s]*"[\s]*(.*?)[\s]*"[\s]*$/', $needle, $matches)) {
643 $token = $matches[1];
644 if ($token === '') {
645 return array();
646 } else {
647 return array($token);
648 }
649 }
650
651 // split the needle into smaller parts separated by non-word characters
652 $tokens = preg_split("/\W/u", $needle);
653 // keep just non-empty parts
654 $tokens = array_filter($tokens);
655 // distinct
656 $tokens = array_unique($tokens);
657 // drop one-letter tokens
658 foreach ($tokens as $ix => $token) {
659 if (strlen($token) == 1) {
660 unset($tokens[$ix]);
661 }
662 }
663
664 return array_values($tokens);
665 }
666
d22e9e32 667 // //////////////////////////////////////////////////////////////////////////
64402867 668
4333580e
DM
669 /**
670 * Make sure that the given properties were set to some not-null value
671 *
672 * @param array $properties the list of properties
673 * @throws coding_exception
674 */
675 private function ensure_isset(array $properties) {
676 foreach ($properties as $property) {
677 if (!isset($this->$property)) {
9b8550f8 678 throw new coding_exception('The property "'.$property.'" is not set.');
4333580e
DM
679 }
680 }
681 }
682}