[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/grade/grading/ -> lib.php (source)

   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  /**
  28   * Factory method returning an instance of the grading manager
  29   *
  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   *
  36   * @category grading
  37   * @example $manager = get_grading_manager($areaid);
  38   * @example $manager = get_grading_manager(context_system::instance());
  39   * @example $manager = get_grading_manager($context, 'mod_assignment', 'submission');
  40   * @param stdClass|int|null $context_or_areaid if $areaid is passed, no other parameter is needed
  41   * @param string|null $component the frankenstyle name of the component
  42   * @param string|null $area the name of the gradable area
  43   * @return grading_manager
  44   */
  45  function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
  46      global $DB;
  47  
  48      $manager = new grading_manager();
  49  
  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  
  61      if (!is_null($context)) {
  62          $manager->set_context($context);
  63      }
  64  
  65      if (!is_null($component)) {
  66          $manager->set_component($component);
  67      }
  68  
  69      if (!is_null($area)) {
  70          $manager->set_area($area);
  71      }
  72  
  73      return $manager;
  74  }
  75  
  76  /**
  77   * General class providing access to common grading features
  78   *
  79   * Grading manager provides access to the particular grading method controller
  80   * in that area.
  81   *
  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
  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
  86   * manager. Such pattern is used when copying form definitions, for example.
  87   *
  88   * @package    core_grading
  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
  92   */
  93  class 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 */
 102      protected $area;
 103  
 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  
 107      /**
 108       * Returns grading manager context
 109       *
 110       * @return stdClass grading manager context
 111       */
 112      public function get_context() {
 113          return $this->context;
 114      }
 115  
 116      /**
 117       * Sets the context the manager operates on
 118       *
 119       * @param stdClass $context
 120       */
 121      public function set_context(stdClass $context) {
 122          $this->areacache = null;
 123          $this->context = $context;
 124      }
 125  
 126      /**
 127       * Returns grading manager component
 128       *
 129       * @return string grading manager component
 130       */
 131      public function get_component() {
 132          return $this->component;
 133      }
 134  
 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) {
 141          $this->areacache = null;
 142          list($type, $name) = core_component::normalize_component($component);
 143          $this->component = $type.'_'.$name;
 144      }
 145  
 146      /**
 147       * Returns grading manager area name
 148       *
 149       * @return string grading manager area name
 150       */
 151      public function get_area() {
 152          return $this->area;
 153      }
 154  
 155      /**
 156       * Sets the area the manager operates on
 157       *
 158       * @param string $area the name of the gradable area
 159       */
 160      public function set_area($area) {
 161          $this->areacache = null;
 162          $this->area = $area;
 163      }
 164  
 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'));
 177  
 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  
 188              if (strval($cm->name) !== '') {
 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  
 195          } else {
 196              throw new coding_exception('Unsupported gradable area context level');
 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  
 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              }
 221  
 222          } else {
 223              throw new coding_exception('Unsupported context level');
 224          }
 225      }
 226  
 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);
 236          $this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST);
 237          $this->component = $this->areacache->component;
 238          $this->area = $this->areacache->areaname;
 239      }
 240  
 241      /**
 242       * Returns the list of installed grading plugins together, optionally extended
 243       * with a simple direct grading.
 244       *
 245       * @param bool $includenone should the 'Simple direct grading' be included
 246       * @return array of the (string)name => (string)localized title of the method
 247       */
 248      public static function available_methods($includenone = true) {
 249  
 250          if ($includenone) {
 251              $list = array('' => get_string('gradingmethodnone', 'core_grading'));
 252          } else {
 253              $list = array();
 254          }
 255  
 256          foreach (core_component::get_plugin_list('gradingform') as $name => $location) {
 257              $list[$name] = get_string('pluginname', 'gradingform_'.$name);
 258          }
 259  
 260          return $list;
 261      }
 262  
 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  
 291          list($plugintype, $pluginname) = core_component::normalize_component($component);
 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  
 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  
 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  
 324          } else if ($this->get_context()->contextlevel == CONTEXT_MODULE) {
 325              list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
 326              return self::available_areas('mod_'.$cm->modname);
 327  
 328          } else {
 329              throw new coding_exception('Unsupported gradable area context level');
 330          }
 331      }
 332  
 333      /**
 334       * Returns the currently active grading method in the gradable area
 335       *
 336       * @return string|null the name of the grading plugin of null if it has not been set
 337       */
 338      public function get_active_method() {
 339          global $DB;
 340  
 341          $this->ensure_isset(array('context', 'component', 'area'));
 342  
 343          // get the current grading area record if it exists
 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          }
 351  
 352          if ($this->areacache === false) {
 353              // no area record yet
 354              return null;
 355          }
 356  
 357          return $this->areacache->activemethod;
 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)
 364       * @return bool true if the method changed or was just set, false otherwise
 365       */
 366      public function set_active_method($method) {
 367          global $DB;
 368  
 369          $this->ensure_isset(array('context', 'component', 'area'));
 370  
 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              }
 382          }
 383  
 384          // get the current grading area record if it exists
 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          }
 392  
 393          $methodchanged = false;
 394  
 395          if ($this->areacache === false) {
 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);
 403              $methodchanged = true;
 404  
 405          } else {
 406              // update the existing record if needed
 407              if ($this->areacache->activemethod !== $method) {
 408                  $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
 409                  $methodchanged = true;
 410              }
 411          }
 412  
 413          $this->areacache = null;
 414  
 415          return $methodchanged;
 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) {
 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;
 436  
 437          } else if (count($areas) == 1) {
 438              // make just a single node for the management screen
 439              $areatitle = reset($areas);
 440              $areaname  = key($areas);
 441              $this->set_area($areaname);
 442              $method = $this->get_active_method();
 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);
 448              }
 449  
 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                  }
 463              }
 464          }
 465      }
 466  
 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  
 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) {
 495          global $CFG, $DB;
 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');
 529          $classname = 'gradingform_'.$method.'_controller';
 530  
 531          return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
 532      }
 533  
 534      /**
 535       * Returns the controller for the active method if it is available
 536       *
 537       * @return null|grading_controller
 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  
 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       */
 555      public function get_management_url(moodle_url $returnurl = null) {
 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);
 563              if ($this->area) {
 564                  $params['area'] = $this->area;
 565              }
 566          }
 567  
 568          if (!is_null($returnurl)) {
 569              $params['returnurl'] = $returnurl->out(false);
 570          }
 571  
 572          return new moodle_url('/grade/grading/manage.php', $params);
 573      }
 574  
 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(
 591              'contextid'     => context_system::instance()->id,
 592              'component'     => 'core_grading',
 593              'areaname'      => $name,
 594              'activemethod'  => $method);
 595          return $DB->insert_record('grading_areas', $area);
 596      }
 597  
 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  
 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  
 667      // //////////////////////////////////////////////////////////////////////////
 668  
 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)) {
 678                  throw new coding_exception('The property "'.$property.'" is not set.');
 679              }
 680          }
 681      }
 682  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1