Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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.
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
 *
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
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
use core_grades\component_gradeitems;
28
 
29
/**
30
 * Factory method returning an instance of the grading manager
31
 *
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.
37
 *
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
46
 */
47
function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
48
    global $DB;
49
 
50
    $manager = new grading_manager();
51
 
52
    if (is_object($context_or_areaid)) {
53
        $context = $context_or_areaid;
54
    } else {
55
        $context = null;
56
 
57
        if (is_numeric($context_or_areaid)) {
58
            $manager->load($context_or_areaid);
59
            return $manager;
60
        }
61
    }
62
 
63
    if (!is_null($context)) {
64
        $manager->set_context($context);
65
    }
66
 
67
    if (!is_null($component)) {
68
        $manager->set_component($component);
69
    }
70
 
71
    if (!is_null($area)) {
72
        $manager->set_area($area);
73
    }
74
 
75
    return $manager;
76
}
77
 
78
/**
79
 * General class providing access to common grading features
80
 *
81
 * Grading manager provides access to the particular grading method controller
82
 * in that area.
83
 *
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.
89
 *
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
94
 */
95
class grading_manager {
96
 
97
    /** @var stdClass the context */
98
    protected $context;
99
 
100
    /** @var string the frankenstyle name of the component */
101
    protected $component;
102
 
103
    /** @var string the name of the gradable area */
104
    protected $area;
105
 
106
    /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
107
    private $areacache = null;
108
 
109
    /**
110
     * Returns grading manager context
111
     *
112
     * @return stdClass grading manager context
113
     */
114
    public function get_context() {
115
        return $this->context;
116
    }
117
 
118
    /**
119
     * Sets the context the manager operates on
120
     *
121
     * @param stdClass $context
122
     */
123
    public function set_context(stdClass $context) {
124
        $this->areacache = null;
125
        $this->context = $context;
126
    }
127
 
128
    /**
129
     * Returns grading manager component
130
     *
131
     * @return string grading manager component
132
     */
133
    public function get_component() {
134
        return $this->component;
135
    }
136
 
137
    /**
138
     * Sets the component the manager operates on
139
     *
140
     * @param string $component the frankenstyle name of the component
141
     */
142
    public function set_component($component) {
143
        $this->areacache = null;
144
        list($type, $name) = core_component::normalize_component($component);
145
        $this->component = $type.'_'.$name;
146
    }
147
 
148
    /**
149
     * Returns grading manager area name
150
     *
151
     * @return string grading manager area name
152
     */
153
    public function get_area() {
154
        return $this->area;
155
    }
156
 
157
    /**
158
     * Sets the area the manager operates on
159
     *
160
     * @param string $area the name of the gradable area
161
     */
162
    public function set_area($area) {
163
        $this->areacache = null;
164
        $this->area = $area;
165
    }
166
 
167
    /**
168
     * Returns a text describing the context and the component
169
     *
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.
173
     *
174
     * @return string
175
     */
176
    public function get_component_title() {
177
 
178
        $this->ensure_isset(array('context', 'component'));
179
 
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');
185
            }
186
 
187
        } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
188
            list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
189
 
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();
195
            }
196
 
197
        } else {
198
            throw new coding_exception('Unsupported gradable area context level');
199
        }
200
 
201
        return $title;
202
    }
203
 
204
    /**
205
     * Returns the localized title of the currently set area
206
     *
207
     * @return string
208
     */
209
    public function get_area_title() {
210
 
211
        if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
212
            return '';
213
 
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 '???';
222
            }
223
 
224
        } else {
225
            throw new coding_exception('Unsupported context level');
226
        }
227
    }
228
 
229
    /**
230
     * Loads the gradable area info from the database
231
     *
232
     * @param int $areaid
233
     */
234
    public function load($areaid) {
235
        global $DB;
236
 
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;
241
    }
242
 
243
    /**
244
     * Returns the list of installed grading plugins together, optionally extended
245
     * with a simple direct grading.
246
     *
247
     * @param bool $includenone should the 'Simple direct grading' be included
248
     * @return array of the (string)name => (string)localized title of the method
249
     */
250
    public static function available_methods($includenone = true) {
251
 
252
        if ($includenone) {
253
            $list = array('' => get_string('gradingmethodnone', 'core_grading'));
254
        } else {
255
            $list = array();
256
        }
257
 
258
        foreach (core_component::get_plugin_list('gradingform') as $name => $location) {
259
            $list[$name] = get_string('pluginname', 'gradingform_'.$name);
260
        }
261
 
262
        return $list;
263
    }
264
 
265
    /**
266
     * Returns the list of available grading methods in the given context
267
     *
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.
270
     *
271
     * Requires the context property to be set in advance.
272
     *
273
     * @param bool $includenone should the 'Simple direct grading' be included
274
     * @return array of the (string)name => (string)localized title of the method
275
     */
276
    public function get_available_methods($includenone = true) {
277
        $this->ensure_isset(array('context'));
278
        return self::available_methods($includenone);
279
    }
280
 
281
    /**
282
     * Returns the list of gradable areas provided by the given component
283
     *
284
     * This performs a callback to the library of the relevant plugin to obtain
285
     * the list of supported areas.
286
     *
287
     * @param string $component normalized component name
288
     * @return array of (string)areacode => (string)localized title of the area
289
     */
290
    public static function available_areas($component) {
291
        global $CFG;
292
 
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);
297
            }
298
 
299
            return $result;
300
        }
301
 
302
        list($plugintype, $pluginname) = core_component::normalize_component($component);
303
 
304
        if ($component === 'core_grading') {
305
            return array();
306
 
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
313
                );
314
                return component_callback($component, $callbackfunction, [], []);
315
            }
316
        } else {
317
            throw new coding_exception('Unsupported area location');
318
        }
319
    }
320
 
321
 
322
    /**
323
     * Returns the list of gradable areas in the given context and component
324
     *
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
328
     */
329
    public function get_available_areas() {
330
        global $CFG;
331
 
332
        $this->ensure_isset(array('context', 'component'));
333
 
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();
339
            }
340
 
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}");
346
 
347
        } else {
348
            throw new coding_exception('Unsupported gradable area context level');
349
        }
350
    }
351
 
352
    /**
353
     * Returns the currently active grading method in the gradable area
354
     *
355
     * @return string|null the name of the grading plugin of null if it has not been set
356
     */
357
    public function get_active_method() {
358
        global $DB;
359
 
360
        $this->ensure_isset(array('context', 'component', 'area'));
361
 
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);
369
        }
370
 
371
        if ($this->areacache === false) {
372
            // no area record yet
373
            return null;
374
        }
375
 
376
        return $this->areacache->activemethod;
377
    }
378
 
379
    /**
380
     * Sets the currently active grading method in the gradable area
381
     *
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
384
     */
385
    public function set_active_method($method) {
386
        global $DB;
387
 
388
        $this->ensure_isset(array('context', 'component', 'area'));
389
 
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');
396
            }
397
            $available = $this->get_available_methods(false);
398
            if (!array_key_exists($method, $available)) {
399
                throw new moodle_exception('invalid_method_name', 'core_grading');
400
            }
401
        }
402
 
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);
410
        }
411
 
412
        $methodchanged = false;
413
 
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;
423
 
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;
429
            }
430
        }
431
 
432
        $this->areacache = null;
433
 
434
        return $methodchanged;
435
    }
436
 
437
    /**
438
     * Extends the settings navigation with the grading settings
439
     *
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.
442
     *
443
     * @param settings_navigation $settingsnav {@link settings_navigation}
444
     * @param navigation_node $modulenode {@link navigation_node}
445
     */
446
    public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
447
 
448
        $this->ensure_isset(array('context', 'component'));
449
 
450
        $areas = $this->get_available_areas();
451
 
452
        if (empty($areas)) {
453
            // no money, no funny
454
            return;
455
 
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');
463
        }
464
    }
465
 
466
    /**
467
     * Extends the module navigation with the advanced grading information
468
     *
469
     * This function is called when the context for the page is an activity module with the
470
     * FEATURE_ADVANCED_GRADING.
471
     *
472
     * @param global_navigation $navigation
473
     * @param navigation_node $modulenode
474
     */
475
    public function extend_navigation(global_navigation $navigation, navigation_node $modulenode=null) {
476
        $this->ensure_isset(array('context', 'component'));
477
 
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);
483
            }
484
        }
485
    }
486
 
487
    /**
488
     * Returns the given method's controller in the gradable area
489
     *
490
     * @param string $method the method name, eg 'rubric' (must be available)
491
     * @return gradingform_controller
492
     */
493
    public function get_controller($method) {
494
        global $CFG, $DB;
495
 
496
        $this->ensure_isset(array('context', 'component', 'area'));
497
 
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');
501
        }
502
        $available = $this->get_available_methods(false);
503
        if (!array_key_exists($method, $available)) {
504
            throw new moodle_exception('invalid_method_name', 'core_grading');
505
        }
506
 
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);
514
        }
515
 
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);
525
        }
526
 
527
        require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
528
        $classname = 'gradingform_'.$method.'_controller';
529
 
530
        return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
531
    }
532
 
533
    /**
534
     * Returns the controller for the active method if it is available
535
     *
536
     * @return null|gradingform_controller
537
     */
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;
543
            }
544
        }
545
        return null;
546
    }
547
 
548
    /**
549
     * Returns the URL of the grading area management page
550
     *
551
     * @param moodle_url $returnurl optional URL of the page where the user should be sent back to
552
     * @return moodle_url
553
     */
554
    public function get_management_url(moodle_url $returnurl = null) {
555
 
556
        $this->ensure_isset(array('context', 'component'));
557
 
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;
564
            }
565
        }
566
 
567
        if (!is_null($returnurl)) {
568
            $params['returnurl'] = $returnurl->out(false);
569
        }
570
 
571
        return new moodle_url('/grade/grading/manage.php', $params);
572
    }
573
 
574
    /**
575
     * Creates a new shared area to hold a grading form template
576
     *
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.
579
     *
580
     * @param string $method the name of the plugin we create the area for
581
     * @return int the new area id
582
     */
583
    public function create_shared_area($method) {
584
        global $DB;
585
 
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);
595
    }
596
 
597
    /**
598
     * Removes all data associated with the given context
599
     *
600
     * This is called by {@link context::delete_content()}
601
     *
602
     * @param int $contextid context id
603
     */
604
    public static function delete_all_for_context($contextid) {
605
        global $DB;
606
 
607
        $areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid));
608
        $methods = array_keys(self::available_methods(false));
609
 
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();
615
            }
616
        }
617
 
618
        $DB->delete_records_list('grading_areas', 'id', $areaids);
619
    }
620
 
621
    /**
622
     * Helper method to tokenize the given string
623
     *
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.
628
     *
629
     * Otherwise, string like 'grammar, english language' would be tokenized into
630
     * the three tokens 'grammar', 'english', 'language'.
631
     *
632
     * One-letter tokens like are dropped in non-phrase mode. Repeated tokens are
633
     * returned just once.
634
     *
635
     * @param string $needle
636
     * @return array
637
     */
638
    public static function tokenize($needle) {
639
 
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);
647
            }
648
        }
649
 
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]);
660
            }
661
        }
662
 
663
        return array_values($tokens);
664
    }
665
 
666
    // //////////////////////////////////////////////////////////////////////////
667
 
668
    /**
669
     * Make sure that the given properties were set to some not-null value
670
     *
671
     * @param array $properties the list of properties
672
     * @throws coding_exception
673
     */
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.');
678
            }
679
        }
680
    }
681
}