MDL-29479 Grading method plugins can extend module settings block
[moodle.git] / grade / grading / lib.php
CommitLineData
4333580e
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/**
19 * Advanced grading methods support
20 *
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
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Factory method returning an instance of the grading manager
31 *
32 * @param stdClass $context
33 * @param string $component the frankenstyle name of the component
8a4acb3a 34 * @param string $area the name of the gradable area
4333580e
DM
35 * @return grading_manager
36 */
8a4acb3a 37function get_grading_manager($context = null, $component = null, $area = null) {
4333580e
DM
38
39 $manager = new grading_manager();
40
41 if (!is_null($context)) {
42 $manager->set_context($context);
43 }
44
45 if (!is_null($component)) {
46 $manager->set_component($component);
47 }
48
8a4acb3a
DM
49 if (!is_null($area)) {
50 $manager->set_area($area);
4333580e
DM
51 }
52
53 return $manager;
54}
55
56/**
57 * General class providing access to common grading features
58 *
9b8550f8
DM
59 * Grading manager provides access to the particular grading method controller
60 * in that area.
61 *
4333580e
DM
62 * Fully initialized instance of the grading manager operates over a single
63 * gradable area. It is possible to work with a partially initialized manager
8a4acb3a
DM
64 * that knows just context and component without known area, for example.
65 * It is also possible to change context, component and area of an existing
4333580e
DM
66 * manager. Such pattern is used when copying form definitions, for example.
67 */
68class grading_manager {
69
70 /** @var stdClass the context */
71 protected $context;
72
73 /** @var string the frankenstyle name of the component */
74 protected $component;
75
76 /** @var string the name of the gradable area */
8a4acb3a 77 protected $area;
4333580e 78
9b8550f8
DM
79 /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
80 private $areacache = null;
81
4333580e
DM
82 /**
83 * Sets the context the manager operates on
84 *
85 * @param stdClass $context
86 */
87 public function set_context(stdClass $context) {
9b8550f8 88 $this->areacache = null;
4333580e
DM
89 $this->context = $context;
90 }
91
92 /**
93 * Sets the component the manager operates on
94 *
95 * @param string $component the frankenstyle name of the component
96 */
97 public function set_component($component) {
9b8550f8
DM
98 $this->areacache = null;
99 list($type, $name) = normalize_component($component);
100 $this->component = $type.'_'.$name;
4333580e
DM
101 }
102
103 /**
8a4acb3a 104 * Sets the area the manager operates on
4333580e 105 *
8a4acb3a 106 * @param string $area the name of the gradable area
4333580e 107 */
8a4acb3a 108 public function set_area($area) {
9b8550f8 109 $this->areacache = null;
8a4acb3a 110 $this->area = $area;
4333580e
DM
111 }
112
113 /**
114 * Returns the list of available grading methods in the given context
115 *
116 * Basically this returns the list of installed grading plugins with an empty value
117 * for simple direct grading. In the future, the list of available methods may be
118 * controlled per-context.
119 *
120 * Requires the context property to be set in advance.
6c9e506c
DM
121 *
122 * @param bool $includenone should the 'Simple direct grading' be included
4333580e
DM
123 * @return array of the (string)name => (string)localized title of the method
124 */
6c9e506c 125 public function get_available_methods($includenone = true) {
4333580e
DM
126
127 $this->ensure_isset(array('context'));
128
6c9e506c
DM
129 if ($includenone) {
130 $list = array('' => get_string('gradingmethodnone', 'core_grading'));
131 } else {
132 $list = array();
133 }
134
135 foreach (get_plugin_list('gradingform') as $name => $location) {
136 $list[$name] = get_string('pluginname', 'gradingform_'.$name);
137 }
138
139 return $list;
4333580e
DM
140 }
141
142 /**
143 * Returns the list of gradable areas in the given context and component
144 *
145 * This performs a callback to the library of the relevant plugin to obtain
146 * the list of supported areas.
147 * @return array of (string)areacode => (string)localized title of the area
148 */
149 public function get_available_areas() {
150 global $CFG;
151
152 $this->ensure_isset(array('context', 'component'));
153
154 // example: if the given context+component lead to mod_assignment, this method
155 // will do something like
156 // require_once($CFG->dirroot.'/mod/assignment/lib.php');
157 // return assignment_gradable_area_list();
158
159 // todo - hardcoded list for now
9b8550f8 160 return array('submission' => 'Submissions');
4333580e
DM
161 }
162
163 /**
8a4acb3a 164 * Returns the currently active grading method in the gradable area
4333580e 165 *
64402867 166 * @return string|null the name of the grading plugin of null if it has not been set
4333580e 167 */
8a4acb3a 168 public function get_active_method() {
64402867
DM
169 global $DB;
170
171 $this->ensure_isset(array('context', 'component', 'area'));
172
173 // get the current grading area record if it exists
9b8550f8
DM
174 if (is_null($this->areacache)) {
175 $this->areacache = $DB->get_record('grading_areas', array(
176 'contextid' => $this->context->id,
177 'component' => $this->component,
178 'areaname' => $this->area),
179 '*', IGNORE_MISSING);
180 }
64402867 181
9b8550f8 182 if ($this->areacache === false) {
64402867
DM
183 // no area record yet
184 return null;
185 }
186
9b8550f8 187 return $this->areacache->activemethod;
64402867
DM
188 }
189
190 /**
191 * Sets the currently active grading method in the gradable area
192 *
193 * @param string $method the method name, eg 'rubric' (must be available)
194 */
195 public function set_active_method($method) {
196 global $DB;
197
8a4acb3a 198 $this->ensure_isset(array('context', 'component', 'area'));
64402867
DM
199
200 // make sure the passed method is a valid plugin name
201 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
202 throw new moodle_exception('invalid_method_name', 'core_grading');
203 }
204 $available = $this->get_available_methods(false);
205 if (!array_key_exists($method, $available)) {
206 throw new moodle_exception('invalid_method_name', 'core_grading');
207 }
208
209 // get the current grading area record if it exists
9b8550f8
DM
210 if (is_null($this->areacache)) {
211 $this->areacache = $DB->get_record('grading_areas', array(
212 'contextid' => $this->context->id,
213 'component' => $this->component,
214 'areaname' => $this->area),
215 '*', IGNORE_MISSING);
216 }
64402867 217
9b8550f8 218 if ($this->areacache === false) {
64402867
DM
219 // no area record yet, create one with the active method set
220 $area = array(
221 'contextid' => $this->context->id,
222 'component' => $this->component,
223 'areaname' => $this->area,
224 'activemethod' => $method);
225 $DB->insert_record('grading_areas', $area);
226
227 } else {
228 // update the existing record if needed
9b8550f8
DM
229 if ($this->areacache->activemethod != $method) {
230 $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
64402867
DM
231 }
232 }
9b8550f8
DM
233
234 $this->areacache = null;
235 }
236
237 /**
238 * Extends the settings navigation with the grading settings
239 *
240 * This function is called when the context for the page is an activity module with the
241 * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms.
242 *
243 * @param settings_navigation $settingsnav {@link settings_navigation}
244 * @param navigation_node $modulenode {@link navigation_node}
245 */
246 public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
247 global $PAGE, $CFG;
248
249 $this->ensure_isset(array('context', 'component'));
250
251 $areas = $this->get_available_areas();
252
253 if (empty($areas)) {
254 // no money, no funny
255 return;
256 }
257
258 foreach ($areas as $areaname => $areatitle) {
259 $this->set_area($areaname);
260 $method = $this->get_active_method();
261
262 if (empty($method)) {
263 // no grading method selected for the given area - nothing to display
264 continue;
265 }
266
267 if (count($areas) > 1) {
268 // if the module supports multiple gradable areas, make a node for each of them
269 $node = $modulenode->add(get_string('gradinginarea', 'core_grading', $areatitle), null, settings_navigation::NODETYPE_BRANCH);
270 } else {
271 // otherwise put the items directly into the module's node
272 $node = $modulenode;
273 }
274
275 $controller = $this->get_controller($method);
276 $controller->extend_settings_navigation($settingsnav, $node);
277 }
278 }
279
280 /**
281 * Returns the given method's controller in the gradable area
282 *
283 * @param string $method the method name, eg 'rubric' (must be available)
284 * @return grading_controller
285 */
286 public function get_controller($method) {
287 global $CFG;
288
289 $this->ensure_isset(array('context', 'component', 'area'));
290
291 // make sure the passed method is a valid plugin name
292 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
293 throw new moodle_exception('invalid_method_name', 'core_grading');
294 }
295 $available = $this->get_available_methods(false);
296 if (!array_key_exists($method, $available)) {
297 throw new moodle_exception('invalid_method_name', 'core_grading');
298 }
299
300 // get the current grading area record if it exists
301 if (is_null($this->areacache)) {
302 $this->areacache = $DB->get_record('grading_areas', array(
303 'contextid' => $this->context->id,
304 'component' => $this->component,
305 'areaname' => $this->area),
306 '*', IGNORE_MISSING);
307 }
308
309 if ($this->areacache === false) {
310 // no area record yet, create one
311 $area = array(
312 'contextid' => $this->context->id,
313 'component' => $this->component,
314 'areaname' => $this->area);
315 $areaid = $DB->insert_record('grading_areas', $area);
316 // reload the cache
317 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
318 }
319
320 require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
321 $classname = $method.'_grading_controller';
322
323 return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
4333580e
DM
324 }
325
9b8550f8 326 ////////////////////////////////////////////////////////////////////////////
64402867 327
4333580e
DM
328 /**
329 * Make sure that the given properties were set to some not-null value
330 *
331 * @param array $properties the list of properties
332 * @throws coding_exception
333 */
334 private function ensure_isset(array $properties) {
335 foreach ($properties as $property) {
336 if (!isset($this->$property)) {
9b8550f8 337 throw new coding_exception('The property "'.$property.'" is not set.');
4333580e
DM
338 }
339 }
340 }
341}