Saving a form as a public template
[moodle.git] / grade / grading / lib.php
1 <?php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Factory method returning an instance of the grading manager
31  *
32  * @param stdClass|int $context or $areaid
33  * @param string $component the frankenstyle name of the component
34  * @param string $area the name of the gradable area
35  * @return grading_manager
36  */
37 function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
38     global $DB;
40     $manager = new grading_manager();
42     if (is_object($context_or_areaid)) {
43         $context = $context_or_areaid;
44     } else {
45         $context = null;
47         if (is_numeric($context_or_areaid)) {
48             $manager->load($context_or_areaid);
49             return $manager;
50         }
51     }
53     if (!is_null($context)) {
54         $manager->set_context($context);
55     }
57     if (!is_null($component)) {
58         $manager->set_component($component);
59     }
61     if (!is_null($area)) {
62         $manager->set_area($area);
63     }
65     return $manager;
66 }
68 /**
69  * General class providing access to common grading features
70  *
71  * Grading manager provides access to the particular grading method controller
72  * in that area.
73  *
74  * Fully initialized instance of the grading manager operates over a single
75  * gradable area. It is possible to work with a partially initialized manager
76  * that knows just context and component without known area, for example.
77  * It is also possible to change context, component and area of an existing
78  * manager. Such pattern is used when copying form definitions, for example.
79  */
80 class grading_manager {
82     /** @var stdClass the context */
83     protected $context;
85     /** @var string the frankenstyle name of the component */
86     protected $component;
88     /** @var string the name of the gradable area */
89     protected $area;
91     /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
92     private $areacache = null;
94     /**
95      * @return stdClass grading manager context
96      */
97     public function get_context() {
98         return $this->context;
99     }
101     /**
102      * Sets the context the manager operates on
103      *
104      * @param stdClass $context
105      */
106     public function set_context(stdClass $context) {
107         $this->areacache = null;
108         $this->context = $context;
109     }
111     /**
112      * @return string grading manager component
113      */
114     public function get_component() {
115         return $this->component;
116     }
118     /**
119      * Sets the component the manager operates on
120      *
121      * @param string $component the frankenstyle name of the component
122      */
123     public function set_component($component) {
124         $this->areacache = null;
125         list($type, $name) = normalize_component($component);
126         $this->component = $type.'_'.$name;
127     }
129     /**
130      * @return string grading manager area name
131      */
132     public function get_area() {
133         return $this->area;
134     }
136     /**
137      * Sets the area the manager operates on
138      *
139      * @param string $area the name of the gradable area
140      */
141     public function set_area($area) {
142         $this->areacache = null;
143         $this->area = $area;
144     }
146     /**
147      * Returns a text describing the context and the component
148      *
149      * At the moment this works for gradable areas in course modules. In the future, this
150      * method should be improved so it works for other contexts (blocks, gradebook items etc)
151      * or subplugins.
152      *
153      * @return string
154      */
155     public function get_component_title() {
157         $this->ensure_isset(array('context', 'component'));
159         if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
160             if ($this->get_component() == 'core_grading') {
161                 $title = ''; // we are in the bank UI
162             } else {
163                 throw new coding_exception('Unsupported component at the system context');
164             }
166         } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
167             list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
169             if (!empty($cm->name)) {
170                 $title = $cm->name;
171             } else {
172                 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
173                 $title = $this->get_component();
174             }
176         } else {
177             throw new coding_exception('Unsupported gradable area context level');
178         }
180         return $title;
181     }
183     /**
184      * Returns the localized title of the currently set area
185      *
186      * @return string
187      */
188     public function get_area_title() {
190         if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
191             return '';
193         } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
194             $this->ensure_isset(array('context', 'component', 'area'));
195             $areas = $this->get_available_areas();
196             if (array_key_exists($this->get_area(), $areas)) {
197                 return $areas[$this->get_area()];
198             } else {
199                 debugging('Unknown area!');
200                 return '???';
201             }
203         } else {
204             throw new coding_exception('Unsupported context level');
205         }
206     }
208     /**
209      * Loads the gradable area info from the database
210      *
211      * @param int $areaid
212      */
213     public function load($areaid) {
214         global $DB;
216         $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
217         $this->context = get_context_instance_by_id($this->areacache->contextid, MUST_EXIST);
218         $this->component = $this->areacache->component;
219         $this->area = $this->areacache->areaname;
220     }
222     /**
223      * Returns the list of available grading methods in the given context
224      *
225      * Basically this returns the list of installed grading plugins with an empty value
226      * for simple direct grading. In the future, the list of available methods may be
227      * controlled per-context.
228      *
229      * Requires the context property to be set in advance.
230      *
231      * @param bool $includenone should the 'Simple direct grading' be included
232      * @return array of the (string)name => (string)localized title of the method
233      */
234     public function get_available_methods($includenone = true) {
236         $this->ensure_isset(array('context'));
238         if ($includenone) {
239             $list = array('' => get_string('gradingmethodnone', 'core_grading'));
240         } else {
241             $list = array();
242         }
244         foreach (get_plugin_list('gradingform') as $name => $location) {
245             $list[$name] = get_string('pluginname', 'gradingform_'.$name);
246         }
248         return $list;
249     }
251     /**
252      * Returns the list of gradable areas in the given context and component
253      *
254      * This performs a callback to the library of the relevant plugin to obtain
255      * the list of supported areas.
256      * @return array of (string)areacode => (string)localized title of the area
257      */
258     public function get_available_areas() {
259         global $CFG;
261         $this->ensure_isset(array('context', 'component'));
263         // example: if the given context+component lead to mod_assignment, this method
264         // will do something like
265         // require_once($CFG->dirroot.'/mod/assignment/lib.php');
266         // return assignment_gradable_area_list();
268         // todo - what to return for bank areas in the system context
269         // todo - hardcoded list for now
270         return array('submission' => 'Submissions');
271     }
273     /**
274      * Returns the currently active grading method in the gradable area
275      *
276      * @return string|null the name of the grading plugin of null if it has not been set
277      */
278     public function get_active_method() {
279         global $DB;
281         $this->ensure_isset(array('context', 'component', 'area'));
283         // get the current grading area record if it exists
284         if (is_null($this->areacache)) {
285             $this->areacache = $DB->get_record('grading_areas', array(
286                 'contextid' => $this->context->id,
287                 'component' => $this->component,
288                 'areaname'  => $this->area),
289             '*', IGNORE_MISSING);
290         }
292         if ($this->areacache === false) {
293             // no area record yet
294             return null;
295         }
297         return $this->areacache->activemethod;
298     }
300     /**
301      * Sets the currently active grading method in the gradable area
302      *
303      * @param string $method the method name, eg 'rubric' (must be available)
304      * @return bool true if the method changed or was just set, false otherwise
305      */
306     public function set_active_method($method) {
307         global $DB;
309         $this->ensure_isset(array('context', 'component', 'area'));
311         // make sure the passed method is empty or a valid plugin name
312         if (empty($method)) {
313             $method = null;
314         } else {
315             if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
316                 throw new moodle_exception('invalid_method_name', 'core_grading');
317             }
318             $available = $this->get_available_methods(false);
319             if (!array_key_exists($method, $available)) {
320                 throw new moodle_exception('invalid_method_name', 'core_grading');
321             }
322         }
324         // get the current grading area record if it exists
325         if (is_null($this->areacache)) {
326             $this->areacache = $DB->get_record('grading_areas', array(
327                 'contextid' => $this->context->id,
328                 'component' => $this->component,
329                 'areaname'  => $this->area),
330             '*', IGNORE_MISSING);
331         }
333         $methodchanged = false;
335         if ($this->areacache === false) {
336             // no area record yet, create one with the active method set
337             $area = array(
338                 'contextid'     => $this->context->id,
339                 'component'     => $this->component,
340                 'areaname'      => $this->area,
341                 'activemethod'  => $method);
342             $DB->insert_record('grading_areas', $area);
343             $methodchanged = true;
345         } else {
346             // update the existing record if needed
347             if ($this->areacache->activemethod !== $method) {
348                 $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
349                 $methodchanged = true;
350             }
351         }
353         $this->areacache = null;
355         return $methodchanged;
356     }
358     /**
359      * Extends the settings navigation with the grading settings
360      *
361      * This function is called when the context for the page is an activity module with the
362      * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms.
363      *
364      * @param settings_navigation $settingsnav {@link settings_navigation}
365      * @param navigation_node $modulenode {@link navigation_node}
366      */
367     public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
369         $this->ensure_isset(array('context', 'component'));
371         $areas = $this->get_available_areas();
373         if (empty($areas)) {
374             // no money, no funny
375             return;
377         } else if (count($areas) == 1) {
378             // make just a single node for the management screen
379             $areatitle = reset($areas);
380             $areaname  = key($areas);
381             $this->set_area($areaname);
382             $method = $this->get_active_method();
383             $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
384                 $this->get_management_url(), settings_navigation::TYPE_CUSTOM);
385             if ($method) {
386                 $controller = $this->get_controller($method);
387                 $controller->extend_settings_navigation($settingsnav, $managementnode);
388             }
390         } else {
391             // make management screen node for each area
392             $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
393                 null, settings_navigation::TYPE_CUSTOM);
394             foreach ($areas as $areaname => $areatitle) {
395                 $this->set_area($areaname);
396                 $method = $this->get_active_method();
397                 $node = $managementnode->add($areatitle,
398                     $this->get_management_url(), settings_navigation::TYPE_CUSTOM);
399                 if ($method) {
400                     $controller = $this->get_controller($method);
401                     $controller->extend_settings_navigation($settingsnav, $node);
402                 }
403             }
404         }
405     }
407     /**
408      * Returns the given method's controller in the gradable area
409      *
410      * @param string $method the method name, eg 'rubric' (must be available)
411      * @return grading_controller
412      */
413     public function get_controller($method) {
414         global $CFG;
416         $this->ensure_isset(array('context', 'component', 'area'));
418         // make sure the passed method is a valid plugin name
419         if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
420             throw new moodle_exception('invalid_method_name', 'core_grading');
421         }
422         $available = $this->get_available_methods(false);
423         if (!array_key_exists($method, $available)) {
424             throw new moodle_exception('invalid_method_name', 'core_grading');
425         }
427         // get the current grading area record if it exists
428         if (is_null($this->areacache)) {
429             $this->areacache = $DB->get_record('grading_areas', array(
430                 'contextid' => $this->context->id,
431                 'component' => $this->component,
432                 'areaname'  => $this->area),
433             '*', IGNORE_MISSING);
434         }
436         if ($this->areacache === false) {
437             // no area record yet, create one
438             $area = array(
439                 'contextid' => $this->context->id,
440                 'component' => $this->component,
441                 'areaname'  => $this->area);
442             $areaid = $DB->insert_record('grading_areas', $area);
443             // reload the cache
444             $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
445         }
447         require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
448         $classname = 'gradingform_'.$method.'_controller';
450         return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
451     }
453     /**
454      * Returns the controller for the active method if it is available
455      *
456      * @return null|grading_controller
457      */
458     public function get_active_controller() {
459         if ($gradingmethod = $this->get_active_method()) {
460             $controller = $this->get_controller($gradingmethod);
461             if ($controller->is_form_available()) {
462                 return $controller;
463             }
464         }
465         return null;
466     }
468     /**
469      * Returns the URL of the grading area management page
470      *
471      * @param moodle_url $returnurl optional URL of the page where the user should be sent back to
472      * @return moodle_url
473      */
474     public function get_management_url(moodle_url $returnurl = null) {
476         $this->ensure_isset(array('context', 'component'));
478         if ($this->areacache) {
479             $params = array('areaid' => $this->areacache->id);
480         } else {
481             $params = array('contextid' => $this->context->id, 'component' => $this->component);
482             if ($this->area) {
483                 $params['area'] = $this->area;
484             }
485         }
487         if (!is_null($returnurl)) {
488             $params['returnurl'] = $returnurl->out(false);
489         }
491         return new moodle_url('/grade/grading/manage.php', $params);
492     }
494     /**
495      * Creates a new shared area to hold a grading form template
496      *
497      * Shared area are implemented as virtual gradable areas at the system level context
498      * with the component set to core_grading and unique random area name.
499      *
500      * @param string $method the name of the plugin we create the area for
501      * @return int the new area id
502      */
503     public function create_shared_area($method) {
504         global $DB;
506         // generate some unique random name for the new area
507         $name = $method . '_' . sha1(rand().uniqid($method, true));
508         // create new area record
509         $area = array(
510             'contextid'     => get_system_context()->id,
511             'component'     => 'core_grading',
512             'areaname'      => $name,
513             'activemethod'  => $method);
514         return $DB->insert_record('grading_areas', $area);
515     }
517     ////////////////////////////////////////////////////////////////////////////
519     /**
520      * Make sure that the given properties were set to some not-null value
521      *
522      * @param array $properties the list of properties
523      * @throws coding_exception
524      */
525     private function ensure_isset(array $properties) {
526         foreach ($properties as $property) {
527             if (!isset($this->$property)) {
528                 throw new coding_exception('The property "'.$property.'" is not set.');
529             }
530         }
531     }