Merge branch 'MDL-76800-master' of https://github.com/raortegar/moodle
[moodle.git] / grade / grading / lib.php
blobbeeade0fcf6821d8f8bcacc0f3cf971a73efcbe2
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.
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 * Advanced grading methods support
20 * @package core_grading
21 * @copyright 2011 David Mudrak <david@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 use core_grades\component_gradeitems;
29 /**
30 * Factory method returning an instance of the grading manager
32 * There are basically ways how to use this factory method. If the area record
33 * id is known to the caller, get the manager for that area by providing just
34 * the id. If the area record id is not know, the context, component and area name
35 * can be provided. Note that null values are allowed in the second case as the context,
36 * component and the area name can be set explicitly later.
38 * @category grading
39 * @example $manager = get_grading_manager($areaid);
40 * @example $manager = get_grading_manager(context_system::instance());
41 * @example $manager = get_grading_manager($context, 'mod_assign', 'submission');
42 * @param stdClass|int|null $context_or_areaid if $areaid is passed, no other parameter is needed
43 * @param string|null $component the frankenstyle name of the component
44 * @param string|null $area the name of the gradable area
45 * @return grading_manager
47 function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
48 global $DB;
50 $manager = new grading_manager();
52 if (is_object($context_or_areaid)) {
53 $context = $context_or_areaid;
54 } else {
55 $context = null;
57 if (is_numeric($context_or_areaid)) {
58 $manager->load($context_or_areaid);
59 return $manager;
63 if (!is_null($context)) {
64 $manager->set_context($context);
67 if (!is_null($component)) {
68 $manager->set_component($component);
71 if (!is_null($area)) {
72 $manager->set_area($area);
75 return $manager;
78 /**
79 * General class providing access to common grading features
81 * Grading manager provides access to the particular grading method controller
82 * in that area.
84 * Fully initialized instance of the grading manager operates over a single
85 * gradable area. It is possible to work with a partially initialized manager
86 * that knows just context and component without known area, for example.
87 * It is also possible to change context, component and area of an existing
88 * manager. Such pattern is used when copying form definitions, for example.
90 * @package core_grading
91 * @copyright 2011 David Mudrak <david@moodle.com>
92 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
93 * @category grading
95 class grading_manager {
97 /** @var stdClass the context */
98 protected $context;
100 /** @var string the frankenstyle name of the component */
101 protected $component;
103 /** @var string the name of the gradable area */
104 protected $area;
106 /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
107 private $areacache = null;
110 * Returns grading manager context
112 * @return stdClass grading manager context
114 public function get_context() {
115 return $this->context;
119 * Sets the context the manager operates on
121 * @param stdClass $context
123 public function set_context(stdClass $context) {
124 $this->areacache = null;
125 $this->context = $context;
129 * Returns grading manager component
131 * @return string grading manager component
133 public function get_component() {
134 return $this->component;
138 * Sets the component the manager operates on
140 * @param string $component the frankenstyle name of the component
142 public function set_component($component) {
143 $this->areacache = null;
144 list($type, $name) = core_component::normalize_component($component);
145 $this->component = $type.'_'.$name;
149 * Returns grading manager area name
151 * @return string grading manager area name
153 public function get_area() {
154 return $this->area;
158 * Sets the area the manager operates on
160 * @param string $area the name of the gradable area
162 public function set_area($area) {
163 $this->areacache = null;
164 $this->area = $area;
168 * Returns a text describing the context and the component
170 * At the moment this works for gradable areas in course modules. In the future, this
171 * method should be improved so it works for other contexts (blocks, gradebook items etc)
172 * or subplugins.
174 * @return string
176 public function get_component_title() {
178 $this->ensure_isset(array('context', 'component'));
180 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
181 if ($this->get_component() == 'core_grading') {
182 $title = ''; // we are in the bank UI
183 } else {
184 throw new coding_exception('Unsupported component at the system context');
187 } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
188 list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
190 if ($cm && strval($cm->name) !== '') {
191 $title = format_string($cm->name, true, array('context' => $context));
192 } else {
193 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
194 $title = $this->get_component();
197 } else {
198 throw new coding_exception('Unsupported gradable area context level');
201 return $title;
205 * Returns the localized title of the currently set area
207 * @return string
209 public function get_area_title() {
211 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
212 return '';
214 } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
215 $this->ensure_isset(array('context', 'component', 'area'));
216 $areas = $this->get_available_areas();
217 if (array_key_exists($this->get_area(), $areas)) {
218 return $areas[$this->get_area()];
219 } else {
220 debugging('Unknown area!');
221 return '???';
224 } else {
225 throw new coding_exception('Unsupported context level');
230 * Loads the gradable area info from the database
232 * @param int $areaid
234 public function load($areaid) {
235 global $DB;
237 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
238 $this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST);
239 $this->component = $this->areacache->component;
240 $this->area = $this->areacache->areaname;
244 * Returns the list of installed grading plugins together, optionally extended
245 * with a simple direct grading.
247 * @param bool $includenone should the 'Simple direct grading' be included
248 * @return array of the (string)name => (string)localized title of the method
250 public static function available_methods($includenone = true) {
252 if ($includenone) {
253 $list = array('' => get_string('gradingmethodnone', 'core_grading'));
254 } else {
255 $list = array();
258 foreach (core_component::get_plugin_list('gradingform') as $name => $location) {
259 $list[$name] = get_string('pluginname', 'gradingform_'.$name);
262 return $list;
266 * Returns the list of available grading methods in the given context
268 * Currently this is just a static list obtained from {@link self::available_methods()}.
269 * In the future, the list of available methods may be controlled per-context.
271 * Requires the context property to be set in advance.
273 * @param bool $includenone should the 'Simple direct grading' be included
274 * @return array of the (string)name => (string)localized title of the method
276 public function get_available_methods($includenone = true) {
277 $this->ensure_isset(array('context'));
278 return self::available_methods($includenone);
282 * Returns the list of gradable areas provided by the given component
284 * This performs a callback to the library of the relevant plugin to obtain
285 * the list of supported areas.
287 * @param string $component normalized component name
288 * @return array of (string)areacode => (string)localized title of the area
290 public static function available_areas($component) {
291 global $CFG;
293 if (component_gradeitems::defines_advancedgrading_itemnames_for_component($component)) {
294 $result = [];
295 foreach (component_gradeitems::get_advancedgrading_itemnames_for_component($component) as $itemnumber => $itemname) {
296 $result[$itemname] = get_string("gradeitem:{$itemname}", $component);
299 return $result;
302 list($plugintype, $pluginname) = core_component::normalize_component($component);
304 if ($component === 'core_grading') {
305 return array();
307 } else if ($plugintype === 'mod') {
308 $callbackfunction = "grading_areas_list";
309 if (component_callback_exists($component, $callbackfunction)) {
310 debugging(
311 "Components supporting advanced grading should be updated to implement the component_gradeitems class",
312 DEBUG_DEVELOPER
314 return component_callback($component, $callbackfunction, [], []);
316 } else {
317 throw new coding_exception('Unsupported area location');
323 * Returns the list of gradable areas in the given context and component
325 * This performs a callback to the library of the relevant plugin to obtain
326 * the list of supported areas.
327 * @return array of (string)areacode => (string)localized title of the area
329 public function get_available_areas() {
330 global $CFG;
332 $this->ensure_isset(array('context', 'component'));
334 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
335 if ($this->get_component() !== 'core_grading') {
336 throw new coding_exception('Unsupported component at the system context');
337 } else {
338 return array();
341 } else if ($this->get_context()->contextlevel == CONTEXT_MODULE) {
342 $modulecontext = $this->get_context();
343 $coursecontext = $modulecontext->get_course_context();
344 $cm = get_fast_modinfo($coursecontext->instanceid)->get_cm($modulecontext->instanceid);
345 return self::available_areas("mod_{$cm->modname}");
347 } else {
348 throw new coding_exception('Unsupported gradable area context level');
353 * Returns the currently active grading method in the gradable area
355 * @return string|null the name of the grading plugin of null if it has not been set
357 public function get_active_method() {
358 global $DB;
360 $this->ensure_isset(array('context', 'component', 'area'));
362 // get the current grading area record if it exists
363 if (is_null($this->areacache)) {
364 $this->areacache = $DB->get_record('grading_areas', array(
365 'contextid' => $this->context->id,
366 'component' => $this->component,
367 'areaname' => $this->area),
368 '*', IGNORE_MISSING);
371 if ($this->areacache === false) {
372 // no area record yet
373 return null;
376 return $this->areacache->activemethod;
380 * Sets the currently active grading method in the gradable area
382 * @param string $method the method name, eg 'rubric' (must be available)
383 * @return bool true if the method changed or was just set, false otherwise
385 public function set_active_method($method) {
386 global $DB;
388 $this->ensure_isset(array('context', 'component', 'area'));
390 // make sure the passed method is empty or a valid plugin name
391 if (empty($method)) {
392 $method = null;
393 } else {
394 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
395 throw new moodle_exception('invalid_method_name', 'core_grading');
397 $available = $this->get_available_methods(false);
398 if (!array_key_exists($method, $available)) {
399 throw new moodle_exception('invalid_method_name', 'core_grading');
403 // get the current grading area record if it exists
404 if (is_null($this->areacache)) {
405 $this->areacache = $DB->get_record('grading_areas', array(
406 'contextid' => $this->context->id,
407 'component' => $this->component,
408 'areaname' => $this->area),
409 '*', IGNORE_MISSING);
412 $methodchanged = false;
414 if ($this->areacache === false) {
415 // no area record yet, create one with the active method set
416 $area = array(
417 'contextid' => $this->context->id,
418 'component' => $this->component,
419 'areaname' => $this->area,
420 'activemethod' => $method);
421 $DB->insert_record('grading_areas', $area);
422 $methodchanged = true;
424 } else {
425 // update the existing record if needed
426 if ($this->areacache->activemethod !== $method) {
427 $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
428 $methodchanged = true;
432 $this->areacache = null;
434 return $methodchanged;
438 * Extends the settings navigation with the grading settings
440 * This function is called when the context for the page is an activity module with the
441 * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms.
443 * @param settings_navigation $settingsnav {@link settings_navigation}
444 * @param navigation_node $modulenode {@link navigation_node}
446 public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
448 $this->ensure_isset(array('context', 'component'));
450 $areas = $this->get_available_areas();
452 if (empty($areas)) {
453 // no money, no funny
454 return;
456 } else {
457 // make just a single node for the management screen
458 $areatitle = reset($areas);
459 $areaname = key($areas);
460 $this->set_area($areaname);
461 $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
462 $this->get_management_url(), settings_navigation::TYPE_CUSTOM, null, 'advgrading');
467 * Extends the module navigation with the advanced grading information
469 * This function is called when the context for the page is an activity module with the
470 * FEATURE_ADVANCED_GRADING.
472 * @param global_navigation $navigation
473 * @param navigation_node $modulenode
475 public function extend_navigation(global_navigation $navigation, navigation_node $modulenode=null) {
476 $this->ensure_isset(array('context', 'component'));
478 $areas = $this->get_available_areas();
479 foreach ($areas as $areaname => $areatitle) {
480 $this->set_area($areaname);
481 if ($controller = $this->get_active_controller()) {
482 $controller->extend_navigation($navigation, $modulenode);
488 * Returns the given method's controller in the gradable area
490 * @param string $method the method name, eg 'rubric' (must be available)
491 * @return gradingform_controller
493 public function get_controller($method) {
494 global $CFG, $DB;
496 $this->ensure_isset(array('context', 'component', 'area'));
498 // make sure the passed method is a valid plugin name
499 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
500 throw new moodle_exception('invalid_method_name', 'core_grading');
502 $available = $this->get_available_methods(false);
503 if (!array_key_exists($method, $available)) {
504 throw new moodle_exception('invalid_method_name', 'core_grading');
507 // get the current grading area record if it exists
508 if (is_null($this->areacache)) {
509 $this->areacache = $DB->get_record('grading_areas', array(
510 'contextid' => $this->context->id,
511 'component' => $this->component,
512 'areaname' => $this->area),
513 '*', IGNORE_MISSING);
516 if ($this->areacache === false) {
517 // no area record yet, create one
518 $area = array(
519 'contextid' => $this->context->id,
520 'component' => $this->component,
521 'areaname' => $this->area);
522 $areaid = $DB->insert_record('grading_areas', $area);
523 // reload the cache
524 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
527 require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
528 $classname = 'gradingform_'.$method.'_controller';
530 return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
534 * Returns the controller for the active method if it is available
536 * @return null|gradingform_controller
538 public function get_active_controller() {
539 if ($gradingmethod = $this->get_active_method()) {
540 $controller = $this->get_controller($gradingmethod);
541 if ($controller->is_form_available()) {
542 return $controller;
545 return null;
549 * Returns the URL of the grading area management page
551 * @param moodle_url $returnurl optional URL of the page where the user should be sent back to
552 * @return moodle_url
554 public function get_management_url(moodle_url $returnurl = null) {
556 $this->ensure_isset(array('context', 'component'));
558 if ($this->areacache) {
559 $params = array('areaid' => $this->areacache->id);
560 } else {
561 $params = array('contextid' => $this->context->id, 'component' => $this->component);
562 if ($this->area) {
563 $params['area'] = $this->area;
567 if (!is_null($returnurl)) {
568 $params['returnurl'] = $returnurl->out(false);
571 return new moodle_url('/grade/grading/manage.php', $params);
575 * Creates a new shared area to hold a grading form template
577 * Shared area are implemented as virtual gradable areas at the system level context
578 * with the component set to core_grading and unique random area name.
580 * @param string $method the name of the plugin we create the area for
581 * @return int the new area id
583 public function create_shared_area($method) {
584 global $DB;
586 // generate some unique random name for the new area
587 $name = $method . '_' . sha1(rand().uniqid($method, true));
588 // create new area record
589 $area = array(
590 'contextid' => context_system::instance()->id,
591 'component' => 'core_grading',
592 'areaname' => $name,
593 'activemethod' => $method);
594 return $DB->insert_record('grading_areas', $area);
598 * Removes all data associated with the given context
600 * This is called by {@link context::delete_content()}
602 * @param int $contextid context id
604 public static function delete_all_for_context($contextid) {
605 global $DB;
607 $areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid));
608 $methods = array_keys(self::available_methods(false));
610 foreach($areaids as $areaid) {
611 $manager = get_grading_manager($areaid);
612 foreach ($methods as $method) {
613 $controller = $manager->get_controller($method);
614 $controller->delete_definition();
618 $DB->delete_records_list('grading_areas', 'id', $areaids);
622 * Helper method to tokenize the given string
624 * Splits the given string into smaller strings. This is a helper method for
625 * full text searching in grading forms. If the given string is surrounded with
626 * double quotes, the resulting array consists of a single item containing the
627 * quoted content.
629 * Otherwise, string like 'grammar, english language' would be tokenized into
630 * the three tokens 'grammar', 'english', 'language'.
632 * One-letter tokens like are dropped in non-phrase mode. Repeated tokens are
633 * returned just once.
635 * @param string $needle
636 * @return array
638 public static function tokenize($needle) {
640 // check if we are searching for the exact phrase
641 if (preg_match('/^[\s]*"[\s]*(.*?)[\s]*"[\s]*$/', $needle, $matches)) {
642 $token = $matches[1];
643 if ($token === '') {
644 return array();
645 } else {
646 return array($token);
650 // split the needle into smaller parts separated by non-word characters
651 $tokens = preg_split("/\W/u", $needle);
652 // keep just non-empty parts
653 $tokens = array_filter($tokens);
654 // distinct
655 $tokens = array_unique($tokens);
656 // drop one-letter tokens
657 foreach ($tokens as $ix => $token) {
658 if (strlen($token) == 1) {
659 unset($tokens[$ix]);
663 return array_values($tokens);
666 // //////////////////////////////////////////////////////////////////////////
669 * Make sure that the given properties were set to some not-null value
671 * @param array $properties the list of properties
672 * @throws coding_exception
674 private function ensure_isset(array $properties) {
675 foreach ($properties as $property) {
676 if (!isset($this->$property)) {
677 throw new coding_exception('The property "'.$property.'" is not set.');