Merge remote branch 'origin/master' into rubric
authorDavid Mudrak <david@moodle.com>
Thu, 27 Oct 2011 19:52:24 +0000 (21:52 +0200)
committerDavid Mudrak <david@moodle.com>
Thu, 27 Oct 2011 19:58:45 +0000 (21:58 +0200)
Conflicts:
lib/db/upgrade.php

41 files changed:
course/modedit.php
course/moodleform_mod.php
grade/grading/form/lib.php [new file with mode: 0644]
grade/grading/form/rubric/db/install.xml [new file with mode: 0644]
grade/grading/form/rubric/db/upgrade.php [new file with mode: 0644]
grade/grading/form/rubric/edit.php [new file with mode: 0644]
grade/grading/form/rubric/edit_form.php [new file with mode: 0644]
grade/grading/form/rubric/js/rubric.js [new file with mode: 0644]
grade/grading/form/rubric/js/rubriceditor.js [new file with mode: 0644]
grade/grading/form/rubric/lang/en/gradingform_rubric.php [new file with mode: 0644]
grade/grading/form/rubric/lib.php [new file with mode: 0644]
grade/grading/form/rubric/pix/icon.png [new file with mode: 0644]
grade/grading/form/rubric/renderer.php [new file with mode: 0644]
grade/grading/form/rubric/rubriceditor.php [new file with mode: 0644]
grade/grading/form/rubric/styles.css [new file with mode: 0644]
grade/grading/form/rubric/version.php [new file with mode: 0644]
grade/grading/lib.php [new file with mode: 0644]
grade/grading/manage.php [new file with mode: 0644]
grade/grading/renderer.php [new file with mode: 0644]
grade/grading/simpletest/testlib.php [new file with mode: 0644]
grade/grading/yui/manage/manage.js [new file with mode: 0644]
lang/en/grading.php [new file with mode: 0644]
lang/en/plugin.php
lang/en/role.php
lib/db/access.php
lib/db/install.xml
lib/db/upgrade.php
lib/form/grading.php [new file with mode: 0644]
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/pluginlib.php
mod/assignment/lib.php
pix/b/BIG-ICONS [new file with mode: 0644]
pix/b/bookmark-new.png [new file with mode: 0644]
pix/b/document-edit.png [new file with mode: 0644]
pix/b/document-new.png [new file with mode: 0644]
pix/b/document-properties.png [new file with mode: 0644]
pix/b/edit-copy.png [new file with mode: 0644]
pix/b/edit-delete.png [new file with mode: 0644]
theme/standard/style/grade.css

index 111e655..cd931d9 100644 (file)
@@ -148,6 +148,24 @@ if (!empty($add)) {
         $data->introeditor = array('text'=>$currentintro, 'format'=>$data->introformat, 'itemid'=>$draftid_editor);
     }
 
+    if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false)) {
+        require_once($CFG->dirroot.'/grade/grading/lib.php');
+        $gradingman = get_grading_manager($context, 'mod_'.$data->modulename);
+        $data->_advancedgradingdata['methods'] = $gradingman->get_available_methods();
+        $areas = $gradingman->get_available_areas();
+
+        foreach ($areas as $areaname => $areatitle) {
+            $gradingman->set_area($areaname);
+            $method = $gradingman->get_active_method();
+            $data->_advancedgradingdata['areas'][$areaname] = array(
+                'title'  => $areatitle,
+                'method' => $method,
+            );
+            $formfield = 'advancedgradingmethod_'.$areaname;
+            $data->{$formfield} = $method;
+        }
+    }
+
     if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$data->modulename,
                                              'iteminstance'=>$data->instance, 'courseid'=>$course->id))) {
         // add existing outcomes
@@ -560,12 +578,37 @@ if ($mform->is_cancelled()) {
         }
     }
 
+    if (plugin_supports('mod', $fromform->modulename, FEATURE_ADVANCED_GRADING, false)) {
+        require_once($CFG->dirroot.'/grade/grading/lib.php');
+        $context = get_context_instance(CONTEXT_MODULE, $fromform->coursemodule);
+        $gradingman = get_grading_manager($context, 'mod_'.$fromform->modulename);
+        $showgradingmanagement = false;
+        foreach ($gradingman->get_available_areas() as $areaname => $aretitle) {
+            $formfield = 'advancedgradingmethod_'.$areaname;
+            if (isset($fromform->{$formfield})) {
+                $gradingman->set_area($areaname);
+                $methodchanged = $gradingman->set_active_method($fromform->{$formfield});
+                if (empty($fromform->{$formfield})) {
+                    // going back to the simple direct grading is not a reason
+                    // to open the management screen
+                    $methodchanged = false;
+                }
+                $showgradingmanagement = $showgradingmanagement || $methodchanged;
+            }
+        }
+    }
+
     rebuild_course_cache($course->id);
     grade_regrade_final_grades($course->id);
     plagiarism_save_form_elements($fromform); //save plagiarism settings
 
     if (isset($fromform->submitbutton)) {
-        redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$fromform->coursemodule");
+        if (empty($showgradingmanagement)) {
+            redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$fromform->coursemodule");
+        } else {
+            $returnurl = new moodle_url("/mod/$module->name/view.php", array('id' => $fromform->coursemodule));
+            redirect($gradingman->get_management_url($returnurl));
+        }
     } else {
         redirect("$CFG->wwwroot/course/view.php?id={$course->id}#section-{$cw->section}");
     }
index b4fbb2f..a531061 100644 (file)
@@ -87,6 +87,7 @@ abstract class moodleform_mod extends moodleform {
         $this->_features->showdescription   = plugin_supports('mod', $this->_modname, FEATURE_SHOW_DESCRIPTION, false);
 
         $this->_features->gradecat          = ($this->_features->outcomes or $this->_features->hasgrades);
+        $this->_features->advancedgrading   = plugin_supports('mod', $this->_modname, FEATURE_ADVANCED_GRADING, false);
     }
 
     /**
@@ -667,6 +668,33 @@ abstract class moodleform_mod extends moodleform {
                 $mform->setDefault('grade', 100);
             }
 
+            if ($this->_features->advancedgrading
+                    and !empty($this->current->_advancedgradingdata['methods'])
+                    and !empty($this->current->_advancedgradingdata['areas'])) {
+
+                if (count($this->current->_advancedgradingdata['areas']) == 1) {
+                    // if there is just one gradable area (most cases), display just the selector
+                    // without its name to make UI simplier
+                    $areadata = reset($this->current->_advancedgradingdata['areas']);
+                    $areaname = key($this->current->_advancedgradingdata['areas']);
+                    $mform->addElement('select', 'advancedgradingmethod_'.$areaname,
+                        get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']);
+                    $mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading');
+
+                } else {
+                    // the module defines multiple gradable areas, display a selector
+                    // for each of them together with a name of the area
+                    $areasgroup = array();
+                    foreach ($this->current->_advancedgradingdata['areas'] as $areaname => $areadata) {
+                        $areasgroup[] = $mform->createElement('select', 'advancedgradingmethod_'.$areaname,
+                            $areadata['title'], $this->current->_advancedgradingdata['methods']);
+                        $areasgroup[] = $mform->createElement('static', 'advancedgradingareaname_'.$areaname, '', $areadata['title']);
+                    }
+                    $mform->addGroup($areasgroup, 'advancedgradingmethodsgroup', get_string('gradingmethods', 'core_grading'),
+                        array(' ', '<br />'), false);
+                }
+            }
+
             if ($this->_features->gradecat) {
                 $mform->addElement('select', 'gradecat',
                         get_string('gradecategoryonmodform', 'grades'),
diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php
new file mode 100644 (file)
index 0000000..d66ae07
--- /dev/null
@@ -0,0 +1,726 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Common classes used by gradingform plugintypes are defined here
+ *
+ * @package    core
+ * @subpackage grading
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grading method controller represents a plugin used in a particular area
+ */
+abstract class gradingform_controller {
+
+    const DEFINITION_STATUS_WORKINPROGRESS  = 0;
+    const DEFINITION_STATUS_PRIVATE         = 1;
+    const DEFINITION_STATUS_PUBLIC          = 2;
+
+    /** @var stdClass the context */
+    protected $context;
+
+    /** @var string the frankenstyle name of the component */
+    protected $component;
+
+    /** @var string the name of the gradable area */
+    protected $area;
+
+    /** @var int the id of the gradable area record */
+    protected $areaid;
+
+    /** @var stdClass|false the definition structure */
+    protected $definition = false;
+
+    /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */
+    private $graderange = null;
+
+    /**
+     * Do not instantinate this directly, use {@link grading_manager::get_controller()}
+     *
+     * @param stdClass $context the context of the form
+     * @param string $component the frankenstyle name of the component
+     * @param string $area the name of the gradable area
+     * @param int $areaid the id of the gradable area record
+     */
+    public function __construct(stdClass $context, $component, $area, $areaid) {
+        global $DB;
+
+        $this->context      = $context;
+        list($type, $name)  = normalize_component($component);
+        $this->component    = $type.'_'.$name;
+        $this->area         = $area;
+        $this->areaid       = $areaid;
+
+        $this->load_definition();
+    }
+
+    /**
+     * @return stdClass controller context
+     */
+    public function get_context() {
+        return $this->context;
+    }
+
+    /**
+     * @return string gradable component name
+     */
+    public function get_component() {
+        return $this->component;
+    }
+
+    /**
+     * @return string gradable area name
+     */
+    public function get_area() {
+        return $this->area;
+    }
+
+    /**
+     * @return int gradable area id
+     */
+    public function get_areaid() {
+        return $this->areaid;
+    }
+
+    /**
+     * Is the form definition record available?
+     *
+     * Note that this actually checks whether the process of defining the form ever started
+     * and not whether the form definition should be considered as final.
+     *
+     * @return boolean
+     */
+    public function is_form_defined() {
+        return ($this->definition !== false);
+    }
+
+    /**
+     * Is the grading form defined and released for usage by the given user?
+     *
+     * @param int $foruserid the id of the user who attempts to work with the form
+     * @return boolean
+     */
+    public function is_form_available($foruserid = null) {
+        global $USER;
+
+        if (is_null($foruserid)) {
+            $foruserid = $USER->id;
+        }
+
+        if (!$this->is_form_defined()) {
+            return false;
+        }
+
+        if ($this->definition->status == self::DEFINITION_STATUS_PUBLIC) {
+            return true;
+        }
+
+        if ($this->definition->status == self::DEFINITION_STATUS_PRIVATE) {
+            if ($this->definition->usercreated == $foruserid) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns URL of a page where the grading form can be defined and edited.
+     *
+     * @param moodle_url $returnurl optional URL of a page where the user should be sent once they are finished with editing
+     * @return moodle_url
+     */
+    public function get_editor_url(moodle_url $returnurl = null) {
+
+        $params = array('areaid' => $this->areaid);
+
+        if (!is_null($returnurl)) {
+            $params['returnurl'] = $returnurl->out(false);
+        }
+
+        return new moodle_url('/grade/grading/form/'.$this->get_method_name().'/edit.php', $params);
+    }
+
+    /**
+     * Extends the module settings navigation
+     *
+     * This function is called when the context for the page is an activity module with the
+     * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms
+     * and there is an area with the active grading method set to the given plugin.
+     *
+     * @param settings_navigation $settingsnav {@link settings_navigation}
+     * @param navigation_node $node {@link navigation_node}
+     */
+    public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) {
+        // do not extend by default
+    }
+
+    /**
+     * Returns the grading form definition structure
+     *
+     * @param boolean $force whether to force loading from DB even if it was already loaded
+     * @return stdClass|false definition data or false if the form is not defined yet
+     */
+    public function get_definition($force = false) {
+        if ($this->definition === false || $force) {
+            $this->load_definition();
+        }
+        return $this->definition;
+    }
+
+    /**
+     * Returns the form definition suitable for cloning into another area
+     *
+     * @param gradingform_controller $target the controller of the new copy
+     * @return stdClass definition structure to pass to the target's {@link update_definition()}
+     */
+    public function get_definition_copy(gradingform_controller $target) {
+
+        if (get_class($this) != get_class($target)) {
+            throw new coding_exception('The source and copy controller mismatch');
+        }
+
+        if ($target->is_form_defined()) {
+            throw new coding_exception('The target controller already contains a form definition');
+        }
+
+        $old = $this->get_definition();
+        // keep our id
+        $new = new stdClass();
+        $new->copiedfromid = $old->id;
+        $new->name = $old->name;
+        // once we support files embedded into the description, we will want to
+        // relink them into the new file area here (that is why we accept $target)
+        $new->description = $old->description;
+        $new->descriptionformat = $old->descriptionformat;
+        $new->options = $old->options;
+
+        return $new;
+    }
+
+    /**
+     * Saves the defintion data into the database
+     *
+     * The implementation in this base class stores the common data into the record
+     * into the {grading_definition} table. The plugins are likely to extend this
+     * and save their data into own tables, too.
+     *
+     * @param stdClass $definition data containing values for the {grading_definition} table
+     * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
+     */
+    public function update_definition(stdClass $definition, $usermodified = null) {
+        global $DB, $USER;
+
+        if (is_null($usermodified)) {
+            $usermodified = $USER->id;
+        }
+
+        if (!empty($this->definition->id)) {
+            // prepare a record to be updated
+            $record = new stdClass();
+            // populate it with scalar values from the passed definition structure
+            foreach ($definition as $prop => $val) {
+                if (is_array($val) or is_object($val)) {
+                    // probably plugin's data
+                    continue;
+                }
+                $record->{$prop} = $val;
+            }
+            // make sure we do not override some crucial values by accident
+            if (!empty($record->id) and $record->id != $this->definition->id) {
+                throw new coding_exception('Attempting to update other definition record.');
+            }
+            $record->id = $this->definition->id;
+            unset($record->areaid);
+            unset($record->method);
+            unset($record->timecreated);
+            // set the modification flags
+            $record->timemodified = time();
+            $record->usermodified = $usermodified;
+
+            $DB->update_record('grading_definitions', $record);
+
+        } else if ($this->definition === false) {
+            // prepare a record to be inserted
+            $record = new stdClass();
+            // populate it with scalar values from the passed definition structure
+            foreach ($definition as $prop => $val) {
+                if (is_array($val) or is_object($val)) {
+                    // probably plugin's data
+                    continue;
+                }
+                $record->{$prop} = $val;
+            }
+            // make sure we do not override some crucial values by accident
+            if (!empty($record->id)) {
+                throw new coding_exception('Attempting to create a new record while there is already one existing.');
+            }
+            unset($record->id);
+            $record->areaid       = $this->areaid;
+            $record->method       = $this->get_method_name();
+            $record->timecreated  = time();
+            $record->usercreated  = $usermodified;
+            $record->timemodified = $record->timecreated;
+            $record->usermodified = $record->usercreated;
+            $record->status       = self::DEFINITION_STATUS_WORKINPROGRESS;
+
+            $DB->insert_record('grading_definitions', $record);
+
+        } else {
+            throw new coding_exception('Unknown status of the cached definition record.');
+        }
+    }
+
+    /**
+     * Returns the ACTIVE instance for this definition for the specified $raterid and $itemid
+     * (if multiple raters are allowed, or only for $itemid otherwise).
+     *
+     * @param int $raterid
+     * @param int $itemid
+     * @param boolean $idonly
+     * @return mixed if $idonly=true returns id of the found instance, otherwise returns the instance object
+     */
+    public function get_current_instance($raterid, $itemid, $idonly = false) {
+        global $DB;
+        $select = array(
+                'formid'  => $this->definition->id,
+                'itemid' => $itemid,
+                'status'  => gradingform_instance::INSTANCE_STATUS_ACTIVE);
+        if (false /* TODO $manager->allow_multiple_raters() */) {
+            $select['raterid'] = $raterid;
+        }
+        if ($idonly) {
+            if ($current = $DB->get_record('grading_instances', $select, 'id', IGNORE_MISSING)) {
+                return $current->id;
+            }
+        } else {
+            if ($current = $DB->get_record('grading_instances', $select, '*', IGNORE_MISSING)) {
+                return $this->get_instance($current);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns list of active instances for the specified $itemid
+     *
+     * @param int $itemid
+     * @return array of gradingform_instance objects
+     */
+    public function get_current_instances($itemid) {
+        global $DB;
+        $conditions = array('formid'  => $this->definition->id,
+                    'itemid' => $itemid,
+                    'status'  => gradingform_instance::INSTANCE_STATUS_ACTIVE);
+        $records = $DB->get_recordset('grading_instances', $conditions);
+        $rv = array();
+        foreach ($records as $record) {
+            $rv[] = $this->get_instance($record);
+        }
+        return $rv;
+    }
+
+    /**
+     * Returns the object of type gradingform_XXX_instance (where XXX is the plugin method name)
+     *
+     * @param mixed $instance id or row from grading_isntances table
+     * @return gradingform_instance
+     */
+    protected function get_instance($instance) {
+        global $DB;
+        if (is_scalar($instance)) {
+            // instance id is passed as parameter
+            $instance = $DB->get_record('grading_instances', array('id'  => $instance), '*', MUST_EXIST);
+        }
+        if ($instance) {
+            $class = 'gradingform_'. $this->get_method_name(). '_instance';
+            return new $class($this, $instance);
+        }
+        return null;
+    }
+
+    /**
+     * This function is invoked when user (teacher) starts grading.
+     * It creates and returns copy of the current ACTIVE instance if it exists. If this is the
+     * first grading attempt, a new instance is created.
+     * The status of the returned instance is INCOMPLETE
+     *
+     * @param int $raterid
+     * @param int $itemid
+     * @return gradingform_instance
+     */
+    public function create_instance($raterid, $itemid = null) {
+        global $DB;
+        // first find if there is already an active instance for this itemid
+        if ($itemid && $current = $this->get_current_instance($raterid, $itemid)) {
+            return $this->get_instance($current->copy($raterid, $itemid));
+        } else {
+            $class = 'gradingform_'. $this->get_method_name(). '_instance';
+            return $this->get_instance($class::create_new($this->definition->id, $raterid, $itemid));
+        }
+    }
+
+    /**
+     * Returns the HTML code displaying the preview of the grading form
+     *
+     * Plugins are supposed to override/extend this. Ideally they should delegate
+     * the task to their own renderer.
+     *
+     * @param moodle_page $page the target page
+     * @return string
+     */
+    public function render_preview(moodle_page $page) {
+
+        if (!$this->is_form_defined()) {
+            throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined');
+        }
+
+        $output = $page->get_renderer('core_grading');
+
+        return $output->preview_definition_header($this);
+    }
+
+    /**
+     * Deletes the form definition and all the associated data
+     *
+     * @see delete_plugin_definition()
+     * @return void
+     */
+    public function delete_definition() {
+        global $DB;
+
+        if (!$this->is_form_defined()) {
+            // nothing to do
+            return;
+        }
+
+        // firstly, let the plugin delete everything from their own tables
+        $this->delete_plugin_definition();
+        // then, delete all instances left
+        $DB->delete_records('grading_instances', array('formid' => $this->definition->id));
+        // finally, delete the main definition record
+        $DB->delete_records('grading_definitions', array('id' => $this->definition->id));
+
+        $this->definition = false;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Loads the form definition if it exists
+     *
+     * The default implementation just tries to load the record from the {grading_definitions}
+     * table. The plugins are likely to override this with a more complex query that loads
+     * all required data at once.
+     */
+    protected function load_definition() {
+        global $DB;
+        $this->definition = $DB->get_record('grading_definitions', array(
+            'areaid' => $this->areaid,
+            'method' => $this->get_method_name()), '*', IGNORE_MISSING);
+    }
+
+    /**
+     * Deletes all plugin data associated with the given form definiton
+     *
+     * @see delete_definition()
+     */
+    abstract protected function delete_plugin_definition();
+
+    /**
+     * @return string the name of the grading method plugin, eg 'rubric'
+     * @see PARAM_PLUGIN
+     */
+    protected function get_method_name() {
+        if (preg_match('/^gradingform_([a-z][a-z0-9_]*[a-z0-9])_controller$/', get_class($this), $matches)) {
+            return $matches[1];
+        } else {
+            throw new coding_exception('Invalid class name');
+        }
+    }
+
+    /**
+     * Returns html code to be included in student's feedback.
+     *
+     * @param moodle_page $page
+     * @param int $itemid
+     * @param array $grading_info result of function grade_get_grades if plugin want to use some of their info
+     * @param string $defaultcontent default string to be returned if no active grading is found or for some reason can not be shown to a user
+     * @return string
+     */
+    public function render_grade($page, $itemid, $grading_info, $defaultcontent) {
+        return $defaultcontent;
+    }
+
+    /**
+     * Sets the range of grades used in this area. This is usually either range like 0-100
+     * or the scale where keys start from 1. Typical use:
+     * $controller->set_grade_range(make_grades_menu($gradingtype));
+     */
+    public final function set_grade_range(array $graderange) {
+        $this->graderange = $graderange;
+    }
+
+    /**
+     * Returns the range of grades used in this area
+     * @return array
+     */
+    public final function get_grade_range() {
+        if (empty($this->graderange)) {
+            return array();
+        }
+        return $this->graderange;
+    }
+}
+
+/**
+ * Class to manage one grading instance. Stores information and performs actions like
+ * update, copy, validate, submit, etc.
+ *
+ * @copyright  2011 Marina Glancy
+ */
+abstract class gradingform_instance {
+    const INSTANCE_STATUS_ACTIVE = 1;
+    const INSTANCE_STATUS_INCOMPLETE = 0;
+    const INSTANCE_STATUS_ARCHIVE = 3;
+
+    /** @var stdClass record from table grading_instances */
+    protected $data;
+    /** @var gradingform_controller link to the corresponding controller */
+    protected $controller;
+
+    /**
+     * Creates an instance
+     *
+     * @param gradingform_controller $controller
+     * @param stdClass $data
+     */
+    public function __construct($controller, $data) {
+        $this->data = (object)$data;
+        $this->controller = $controller;
+    }
+
+    /**
+     * Creates a new empty instance in DB and mark its status as INCOMPLETE
+     *
+     * @param int $formid
+     * @param int $raterid
+     * @param int $itemid
+     * @return int id of the created instance
+     */
+    public static function create_new($formid, $raterid, $itemid) {
+        global $DB;
+        $instance = new stdClass();
+        $instance->formid = $formid;
+        $instance->raterid = $raterid;
+        $instance->itemid = $itemid;
+        $instance->status = self::INSTANCE_STATUS_INCOMPLETE;
+        $instance->timemodified = time();
+        $instance->feedbackformat = FORMAT_MOODLE;
+        $instanceid = $DB->insert_record('grading_instances', $instance);
+        return $instanceid;
+    }
+
+    /**
+     * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with
+     * the specified values)
+     * Plugins may want to override this function to copy data from additional tables as well
+     *
+     * @param int $raterid value for raterid in the duplicate
+     * @param int $itemid value for itemid in the duplicate
+     * @return int id of the new instance
+     */
+    public function copy($raterid, $itemid) {
+        global $DB;
+        $data = (array)$this->data; // Cast to array to make a copy
+        unset($data['id']);
+        $data['raterid'] = $raterid;
+        $data['itemid'] = $itemid;
+        $data['timemodified'] = time();
+        $data['status'] = self::INSTANCE_STATUS_INCOMPLETE;
+        $instanceid = $DB->insert_record('grading_instances', $data);
+        return $instanceid;
+    }
+
+    /**
+     * Returns the controller
+     *
+     * @return gradingform_controller
+     */
+    public function get_controller() {
+        return $this->controller;
+    }
+
+    /**
+     * Returns instance id
+     *
+     * @return int
+     */
+    public function get_id() {
+        return $this->data->id;
+    }
+
+    /**
+     * Marks the instance as ACTIVE and current active instance (if exists) as ARCHIVE
+     */
+    protected function make_active() {
+        global $DB;
+        if ($this->data->status == self::INSTANCE_STATUS_ACTIVE) {
+            // already active
+            return;
+        }
+        if (empty($this->data->itemid)) {
+            throw new coding_exception('You cannot mark active the grading instance without itemid');
+        }
+        $currentid = $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid, true);
+        if ($currentid) {
+            if ($currentid != $this->get_id()) {
+                $DB->update_record('grading_instances', array('id' => $currentid, 'status' => self::INSTANCE_STATUS_ARCHIVE));
+                $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE));
+            }
+        } else {
+            $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE));
+        }
+        $this->data->status = self::INSTANCE_STATUS_ACTIVE;
+    }
+
+    /**
+     * Deletes this (INCOMPLETE) instance from database. This function is invoked on cancelling the
+     * grading form and/or during cron cleanup.
+     * Plugins using additional tables must override this method to remove additional data.
+     * Note that if the teacher just closes the window or presses 'Back' button of the browser,
+     * this function is not invoked.
+     */
+    public function cancel() {
+        global $DB;
+        // TODO what if we happen delete the ACTIVE instance, shall we rollback to the last ARCHIVE? or throw an exception?
+        // TODO create cleanup cron
+        $DB->delete_records('grading_instances', array('id' => $this->get_id()));
+    }
+
+    /**
+     * Updates the instance with the data received from grading form. This function may be
+     * called via AJAX when grading is not yet completed, so it does not change the
+     * status of the instance.
+     *
+     * @param array $elementvalue
+     */
+    public function update($elementvalue) {
+        global $DB;
+        $newdata = new stdClass();
+        $newdata->id = $this->get_id();
+        $newdata->timemodified = time();
+        if (isset($elementvalue['itemid']) && $elementvalue['itemid'] != $this->data->itemid) {
+            $newdata->itemid = $elementvalue['itemid'];
+        }
+        // TODO also update: rawgrade, feedback, feedbackformat
+        $DB->update_record('grading_instances', $newdata);
+        foreach ($newdata as $key => $value) {
+            $this->data->$key = $value;
+        }
+    }
+
+    /**
+     * Calculates the grade to be pushed to the gradebook
+     *
+     * @return int the valid grade from $this->get_controller()->get_grade_range()
+     */
+    abstract public function get_grade();
+
+    /**
+     * Called when teacher submits the grading form:
+     * updates the instance in DB, marks it as ACTIVE and returns the grade to be pushed to the gradebook.
+     * $itemid must be specified here (it was not required when the instance was
+     * created, because it might not existed in draft)
+     *
+     * @param array $elementvalue
+     * @param int $itemid
+     * @return int the grade on 0-100 scale
+     */
+    public function submit_and_get_grade($elementvalue, $itemid) {
+        $elementvalue['itemid'] = $itemid;
+        $this->update($elementvalue);
+        $this->make_active();
+        return $this->get_grade();
+    }
+
+
+    /**
+     * Returns html for form element of type 'grading'. If there is a form input element
+     * it must have the name $gradingformelement->getName().
+     * If there are more than one input elements they MUST be elements of array with
+     * name $gradingformelement->getName().
+     * Example: {NAME}[myelement1], {NAME}[myelement2][sub1], {NAME}[myelement2][sub2], etc.
+     * ( {NAME} is a shortcut for $gradingformelement->getName() )
+     * After submitting the form the value of $_POST[{NAME}] is passed to the functions
+     * validate_grading_element() and submit_and_get_grade()
+     *
+     * Plugins may use $gradingformelement->getValue() to get the value passed on previous
+     * form submit
+     *
+     * When forming html it is a plugin's responsibility to analyze flags
+     * $gradingformelement->_flagFrozen and $gradingformelement->_persistantFreeze:
+     *
+     * (_flagFrozen == false) => form element is editable
+     *
+     * (_flagFrozen == false && _persistantFreeze == true) => form element is not editable
+     * but all values are passed as hidden elements
+     *
+     * (_flagFrozen == false && _persistantFreeze == false) => form element is not editable
+     * and no values are passed as hidden elements
+     *
+     * Plugins are welcome to use AJAX in the form element. But it is strongly recommended
+     * that the grading only becomes active when teacher presses 'Submit' button (the
+     * method submit_and_get_grade() is invoked)
+     *
+     * Also client-side JS validation may be implemented here
+     *
+     * @see MoodleQuickForm_grading in lib/form/grading.php
+     *
+     * @param moodle_page $page
+     * @param MoodleQuickForm_grading $gradingformelement
+     * @return string
+     */
+    abstract function render_grading_element($page, $gradingformelement);
+
+    /**
+     * Server-side validation of the data received from grading form.
+     *
+     * @param mixed $elementvalue is the scalar or array received in $_POST
+     * @return boolean true if the form data is validated and contains no errors
+     */
+    public function validate_grading_element($elementvalue) {
+        return true;
+    }
+
+    /**
+     * Returns the error message displayed if validation failed.
+     * If plugin wants to display custom message, the empty string should be returned here
+     * and the custom message should be output in render_grading_element()
+     *
+     * @see validate_grading_element()
+     * @return string
+     */
+    public function default_validation_error_message() {
+        return '';
+    }
+}
\ No newline at end of file
diff --git a/grade/grading/form/rubric/db/install.xml b/grade/grading/form/rubric/db/install.xml
new file mode 100644 (file)
index 0000000..2d8eddb
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="grade/grading/form/rubric/db" VERSION="20111014" COMMENT="XMLDB file for Moodle rubrics"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
+>
+  <TABLES>
+    <TABLE NAME="gradingform_rubric_criteria" COMMENT="Stores the rows of the rubric grid." NEXT="gradingform_rubric_levels">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="formid"/>
+        <FIELD NAME="formid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the form definition this criterion is part of" PREVIOUS="id" NEXT="sortorder"/>
+        <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="Defines the order of the criterion in the rubric" PREVIOUS="formid" NEXT="description"/>
+        <FIELD NAME="description" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" COMMENT="The criterion description" PREVIOUS="sortorder" NEXT="descriptionformat"/>
+        <FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="The format of the description field" PREVIOUS="description"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_formid"/>
+        <KEY NAME="fk_formid" TYPE="foreign" FIELDS="formid" REFTABLE="grading_definitions" REFFIELDS="id" PREVIOUS="primary"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="gradingform_rubric_levels" COMMENT="Stores the columns of the rubric grid." PREVIOUS="gradingform_rubric_criteria" NEXT="gradingform_rubric_fillings">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="criterionid"/>
+        <FIELD NAME="criterionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The rubric criterion we are level of" PREVIOUS="id" NEXT="score"/>
+        <FIELD NAME="score" TYPE="number" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" DECIMALS="5" COMMENT="The score for this level" PREVIOUS="criterionid" NEXT="definition"/>
+        <FIELD NAME="definition" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" COMMENT="The optional text describing the level" PREVIOUS="score" NEXT="definitionformat"/>
+        <FIELD NAME="definitionformat" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="The format of the definition field" PREVIOUS="definition"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_criterionid"/>
+        <KEY NAME="fk_criterionid" TYPE="foreign" FIELDS="criterionid" REFTABLE="gradingform_rubric_criteria" REFFIELDS="id" PREVIOUS="primary"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="gradingform_rubric_fillings" COMMENT="Stores the data of how the rubric is filled by a particular rater" PREVIOUS="gradingform_rubric_levels">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="forminstanceid"/>
+        <FIELD NAME="forminstanceid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the grading form instance" PREVIOUS="id" NEXT="criterionid"/>
+        <FIELD NAME="criterionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the criterion (row) in the rubric" PREVIOUS="forminstanceid" NEXT="levelid"/>
+        <FIELD NAME="levelid" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="If a particular level was selected during the assessment, its ID is stored here" PREVIOUS="criterionid" NEXT="remark"/>
+        <FIELD NAME="remark" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" COMMENT="Side note feedback regarding this particular criterion" PREVIOUS="levelid" NEXT="remarkformat"/>
+        <FIELD NAME="remarkformat" TYPE="int" LENGTH="2" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="The format of the remark field" PREVIOUS="remark"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_forminstanceid"/>
+        <KEY NAME="fk_forminstanceid" TYPE="foreign" FIELDS="forminstanceid" REFTABLE="grading_instances" REFFIELDS="id" PREVIOUS="primary" NEXT="fk_criterionid"/>
+        <KEY NAME="fk_criterionid" TYPE="foreign" FIELDS="criterionid" REFTABLE="gradingform_rubric_criteria" REFFIELDS="id" PREVIOUS="fk_forminstanceid" NEXT="uq_instance_criterion"/>
+        <KEY NAME="uq_instance_criterion" TYPE="unique" FIELDS="forminstanceid, criterionid" PREVIOUS="fk_criterionid"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="ix_levelid" UNIQUE="false" FIELDS="levelid" COMMENT="levelid acts as a foreign key but null values are allowed"/>
+      </INDEXES>
+    </TABLE>
+  </TABLES>
+</XMLDB>
\ No newline at end of file
diff --git a/grade/grading/form/rubric/db/upgrade.php b/grade/grading/form/rubric/db/upgrade.php
new file mode 100644 (file)
index 0000000..35fa601
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Keeps track or rubric plugin upgrade path
+ *
+ * @todo get rid of this before merging into the master branch MDL-29798
+ * @param int $oldversion the DB version of currently installed plugin
+ * @return bool true
+ */
+function xmldb_gradingform_rubric_upgrade($oldversion) {
+    global $CFG, $DB, $OUTPUT;
+
+    $dbman = $DB->get_manager();
+
+    if ($oldversion < 2011101400) {
+        // add key uq_instance_criterion (unique)
+        $table = new xmldb_table('gradingform_rubric_fillings');
+        $key = new xmldb_key('uq_instance_criterion', XMLDB_KEY_UNIQUE, array('forminstanceid', 'criterionid'));
+        $dbman->add_key($table, $key);
+        upgrade_plugin_savepoint(true, 2011101400, 'gradingform', 'rubric');
+    }
+
+    if ($oldversion < 2011101401) {
+        // change nullability of field levelid on table gradingform_rubric_fillings to null
+        $table = new xmldb_table('gradingform_rubric_fillings');
+        $field = new xmldb_field('levelid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, 'criterionid');
+        $index = new xmldb_index('ix_levelid', XMLDB_INDEX_NOTUNIQUE, array('levelid'));
+
+        // drop the associated index index first
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        $dbman->change_field_notnull($table, $field);
+
+        // re-create the index now
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        upgrade_plugin_savepoint(true, 2011101401, 'gradingform', 'rubric');
+    }
+
+    return true;
+}
diff --git a/grade/grading/form/rubric/edit.php b/grade/grading/form/rubric/edit.php
new file mode 100644 (file)
index 0000000..a4ac784
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Rubric editor page
+ *
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(dirname(dirname(dirname(dirname(__FILE__))))).'/config.php');
+require_once(dirname(__FILE__).'/lib.php');
+require_once(dirname(__FILE__).'/edit_form.php');
+require_once($CFG->dirroot.'/grade/grading/lib.php');
+
+$areaid = required_param('areaid', PARAM_INT);
+
+$manager = get_grading_manager($areaid);
+
+list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
+
+require_login($course, true, $cm);
+require_capability('moodle/grade:managegradingforms', $context);
+
+$controller = $manager->get_controller('rubric');
+
+$PAGE->set_url(new moodle_url('/grade/grading/form/rubric/edit.php', array('areaid' => $areaid)));
+$PAGE->set_title(get_string('definerubric', 'gradingform_rubric'));
+$PAGE->set_heading(get_string('definerubric', 'gradingform_rubric'));
+$PAGE->requires->js('/grade/grading/form/rubric/js/rubriceditor.js');
+
+//TODO freeze rubric editor if needed
+$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'freezerubric' => optional_param('freeze', 0, PARAM_INT)));
+$data = $controller->get_definition_for_editing();
+$returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL);
+$data->returnurl = $returnurl;
+$mform->set_data($data);
+if ($mform->is_cancelled()) {
+    // todo process editing cancel in a better way
+    redirect($returnurl);
+} else if ($mform->is_submitted() && $mform->is_validated()) {
+    $data = $mform->get_data();
+    $controller->update_definition($data);
+    redirect($returnurl);
+}
+
+echo $OUTPUT->header();
+$mform->display();
+echo $OUTPUT->footer();
\ No newline at end of file
diff --git a/grade/grading/form/rubric/edit_form.php b/grade/grading/form/rubric/edit_form.php
new file mode 100644 (file)
index 0000000..1341dc4
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The form used at the rubric editor page is defined here
+ *
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 Marina Glancy <marina@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+require_once(dirname(__FILE__).'/rubriceditor.php');
+MoodleQuickForm::registerElementType('rubriceditor', $CFG->dirroot.'/grade/grading/form/rubric/rubriceditor.php', 'MoodleQuickForm_rubriceditor');
+
+/**
+ * Defines the rubric edit form
+ */
+class gradingform_rubric_editrubric extends moodleform {
+
+    /**
+     * Form elements definition
+     */
+    public function definition() {
+        $form = $this->_form;
+
+        $form->addElement('hidden', 'areaid');
+        $form->setType('areaid', PARAM_INT);
+
+        $form->addElement('hidden', 'returnurl');
+
+        // name
+        $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size'=>52));
+        $form->addRule('name', get_string('required'), 'required');
+        $form->setType('name', PARAM_TEXT);
+
+        // description
+        $options = gradingform_rubric_controller::description_form_field_options($this->_customdata['context']);
+        $form->addElement('editor', 'description_editor', get_string('description', 'gradingform_rubric'), null, $options);
+        $form->setType('description_editor', PARAM_RAW);
+
+        // rubric editor
+        $element = $form->addElement('rubriceditor', 'rubric', get_string('rubric', 'gradingform_rubric'));
+        $form->setType('rubric', PARAM_RAW);
+        $form->addRule('rubric', '', 'rubriceditorcompleted'); //TODO how to add this rule automatically?????
+        if (array_key_exists('freezerubric', $this->_customdata) && $this->_customdata['freezerubric']) {
+            $element->freeze();
+        }
+
+        $this->add_action_buttons(true);
+    }
+}
diff --git a/grade/grading/form/rubric/js/rubric.js b/grade/grading/form/rubric/js/rubric.js
new file mode 100644 (file)
index 0000000..8c9f232
--- /dev/null
@@ -0,0 +1,30 @@
+M.gradingform_rubric = {};
+
+/**
+ * This function is called for each rubric on page.
+ */
+M.gradingform_rubric.init = function(Y, options) {
+    Y.on('click', M.gradingform_rubric.levelclick, '#rubric-'+options.name+' .level', null, Y, options.name);
+    Y.all('#rubric-'+options.name+' .radio').setStyle('display', 'none')
+    Y.all('#rubric-'+options.name+' .level').each(function (node) {
+      if (node.one('input[type=radio][checked]')) {
+        node.addClass('checked');
+      }
+    });
+};
+
+M.gradingform_rubric.levelclick = function(e, Y, name) {
+    var el = e.target
+    while (el && !el.hasClass('level')) el = el.get('parentNode')
+    if (!el) return
+    e.preventDefault();
+    el.siblings().removeClass('checked');
+    chb = el.one('input[type=radio]')
+    if (!chb.get('checked')) {
+        chb.set('checked', true)
+        el.addClass('checked')
+    } else {
+        el.removeClass('checked');
+        el.get('parentNode').all('input[type=radio]').set('checked', false)
+    }
+}
diff --git a/grade/grading/form/rubric/js/rubriceditor.js b/grade/grading/form/rubric/js/rubriceditor.js
new file mode 100644 (file)
index 0000000..4d1fc76
--- /dev/null
@@ -0,0 +1,171 @@
+M.gradingform_rubriceditor = {'templates' : {}, 'eventhandler' : null};
+
+/**
+ * This function is called for each rubriceditor on page.
+ */
+M.gradingform_rubriceditor.init = function(Y, options) {
+    M.gradingform_rubriceditor.templates[options.name] = {
+        'criterion' : options.criteriontemplate,
+        'level' : options.leveltemplate
+    }
+    M.gradingform_rubriceditor.disablealleditors(null, Y, options.name)
+    M.gradingform_rubriceditor.addhandlers(Y, options.name)
+};
+
+// Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html
+M.gradingform_rubriceditor.addhandlers = function(Y, name) {
+    if (M.gradingform_rubriceditor.eventhandler) M.gradingform_rubriceditor.eventhandler.detach()
+    M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.clickanywhere, 'body', null, Y, name);
+    M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubric-'+name+' input[type=submit]', null, Y, name);
+}
+
+M.gradingform_rubriceditor.disablealleditors = function(e, Y, name) {
+    Y.all('#rubric-'+name+' .level').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} );
+    Y.all('#rubric-'+name+' .description').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} );
+}
+
+M.gradingform_rubriceditor.clickanywhere = function(e, Y, name) {
+    var el = e.target
+    // if clicked on button - disablecurrenteditor, continue
+    if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') {
+        M.gradingform_rubriceditor.disablealleditors(null, Y, name)
+        return
+    }
+    // else if clicked on level and this level is not enabled - enable it
+    // or if clicked on description and this description is not enabled - enable it
+    while (el && !(el.hasClass('level') || el.hasClass('description'))) el = el.get('parentNode')
+    if (el) {
+        if (el.one('textarea').getStyle('display') == 'none') {
+            M.gradingform_rubriceditor.disablealleditors(null, Y, name)
+            M.gradingform_rubriceditor.editmode(el, true)
+        }
+        return
+    }
+    // else disablecurrenteditor
+    M.gradingform_rubriceditor.disablealleditors(null, Y, name)
+}
+
+M.gradingform_rubriceditor.editmode = function(el, editmode) {
+    var ta = el.one('textarea')
+    if (!ta.get('parentNode').one('.plainvalue')) {
+        ta.get('parentNode').append('<div class="plainvalue"></div>')
+    }
+    var tb = el.one('input[type=text]')
+    if (tb && !tb.get('parentNode').one('.plainvalue')) {
+        tb.get('parentNode').append('<div class="plainvalue"></div>')
+    }
+    if (!editmode) {
+        var value = ta.get('value')
+        if (value.length) ta.get('parentNode').one('.plainvalue').removeClass('empty')
+        else {
+            value = (el.hasClass('level')) ? M.str.gradingform_rubric.levelempty : M.str.gradingform_rubric.criterionempty
+            ta.get('parentNode').one('.plainvalue').addClass('empty')
+        }
+        ta.get('parentNode').one('.plainvalue').set('innerHTML', value)
+        ta.get('parentNode').one('.plainvalue').setStyle('display', 'block')
+        ta.setStyle('display', 'none')
+        if (tb) {
+            tb.get('parentNode').one('.plainvalue').set('innerHTML', tb.get('value'))
+            tb.get('parentNode').one('.plainvalue').setStyle('display', 'inline-block')
+            tb.setStyle('display', 'none')
+        }
+    } else {
+        if (tb) {
+            tb.get('parentNode').one('.plainvalue').setStyle('display', 'none')
+            tb.setStyle('display', 'inline-block')
+        }
+        var width = ta.get('parentNode').getComputedStyle('width') // TODO min width
+        var height = ta.get('parentNode').getComputedStyle('height') // TODO min height
+        if (el.hasClass('level')) {
+            height = el.getComputedStyle('height') - el.one('.score').getComputedStyle('height')
+        } else if (el.hasClass('description')) {
+            height = el.get('parentNode').getComputedStyle('height')
+        }
+        ta.get('parentNode').one('.plainvalue').setStyle('display', 'none')
+        ta.setStyle('display', 'block').setStyle('width', width).setStyle('height', height)
+        ta.focus()
+    }
+}
+
+// handler for clicking on submit buttons within rubriceditor element. Adds/deletes/rearranges criteria and/or levels on client side
+M.gradingform_rubriceditor.buttonclick = function(e, Y, name, confirmed) {
+    if (e.target.get('type') != 'submit') return;
+    var chunks = e.target.get('id').split('-'),
+        action = chunks[chunks.length-1]
+    if (chunks[0] != name || chunks[1] != 'criteria') return;
+    var elements_str
+    if (chunks.length>4 || action == 'addlevel') {
+        elements_str = '#rubric-'+name+' #'+name+'-criteria-'+chunks[2]+'-levels .level'
+    } else {
+        elements_str = '#rubric-'+name+' .criterion'
+    }
+    // prepare the id of the next inserted level or criterion
+    var newid = 1
+    if (action == 'addcriterion' || action == 'addlevel') {
+        Y.all(elements_str).each( function(node) {
+            var idchunks = node.get('id').split('-'), id = idchunks.pop();
+            if (id.match(/^NEWID(\d+)$/)) newid = Math.max(newid, parseInt(id.substring(5))+1);
+        } );
+    }
+    var dialog_options = {
+        'scope' : this,
+        'callbackargs' : [e, Y, name, true],
+        'callback' : M.gradingform_rubriceditor.buttonclick
+    };
+    if (chunks.length == 3 && action == 'addcriterion') {
+        // ADD NEW CRITERION
+        var newcriterion = M.gradingform_rubriceditor.templates[name]['criterion'].
+            replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '')
+        var parentel = Y.one('#'+name+'-criteria')
+        if (parentel.one('>tbody')) parentel = parentel.one('>tbody')
+        parentel.append(newcriterion)
+        M.gradingform_rubriceditor.addhandlers(Y, name);
+    } else if (chunks.length == 5 && action == 'addlevel') {
+        // ADD NEW LEVEL
+        var newlevel = M.gradingform_rubriceditor.templates[name]['level'].
+            replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '')
+        Y.one('#'+name+'-criteria-'+chunks[2]+'-levels').append(newlevel)
+        var levels = Y.all('#'+name+'-criteria-'+chunks[2]+'-levels .level')
+        if (levels.size()) levels.set('width', Math.round(100/levels.size())+'%')
+        M.gradingform_rubriceditor.addhandlers(Y, name);
+    } else if (chunks.length == 4 && action == 'moveup') {
+        // MOVE CRITERION UP
+        el = Y.one('#'+name+'-criteria-'+chunks[2])
+        if (el.previous()) el.get('parentNode').insertBefore(el, el.previous())
+    } else if (chunks.length == 4 && action == 'movedown') {
+        // MOVE CRITERION DOWN
+        el = Y.one('#'+name+'-criteria-'+chunks[2])
+        if (el.next()) el.get('parentNode').insertBefore(el.next(), el)
+    } else if (chunks.length == 4 && action == 'delete') {
+        // DELETE CRITERION
+        if (confirmed) {
+            Y.one('#'+name+'-criteria-'+chunks[2]).remove()
+        } else {
+            dialog_options['message'] = M.str.gradingform_rubric.confirmdeletecriterion
+            M.util.show_confirm_dialog(e, dialog_options);
+        }
+    } else if (chunks.length == 6 && action == 'delete') {
+        // DELETE LEVEL
+        if (confirmed) {
+            Y.one('#'+name+'-criteria-'+chunks[2]+'-'+chunks[3]+'-'+chunks[4]).remove()
+            levels = Y.all('#'+name+'-criteria-'+chunks[2]+'-levels .level')
+            if (levels.size()) levels.set('width', Math.round(100/levels.size())+'%')
+        } else {
+            dialog_options['message'] = M.str.gradingform_rubric.confirmdeletelevel
+            M.util.show_confirm_dialog(e, dialog_options);
+        }
+    } else {
+        // unknown action
+        return;
+    }
+    e.preventDefault();
+    // properly set classes and sortorder
+    var elements = Y.all(elements_str)
+    for (var i=0;i<elements.size();i++) {
+        elements.item(i).removeClass('first').removeClass('last').removeClass('even').removeClass('odd').
+            addClass(((i%2)?'odd':'even') + ((i==0)?' first':'') + ((i==elements.size()-1)?' last':''))
+        elements.item(i).all('input[type=hidden]').each(
+            function(node) { if (node.get('name').match(/sortorder/)) node.set('value', i) }
+        );
+    }
+}
diff --git a/grade/grading/form/rubric/lang/en/gradingform_rubric.php b/grade/grading/form/rubric/lang/en/gradingform_rubric.php
new file mode 100644 (file)
index 0000000..21b2bbb
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['definerubric'] = 'Define rubric';
+$string['pluginname'] = 'Rubric';
+
+$string['confirmdeletecriterion'] = 'Are you sure you want to delete this criterion?';
+$string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
+$string['description'] = 'Description';
+$string['name'] = 'Name';
+
+$string['addcriterion'] = 'Add criterion';
+$string['criterionmoveup'] = 'Move up';
+$string['criteriondelete'] = 'Delete criterion';
+$string['criterionmovedown'] = 'Move down';
+$string['criterionaddlevel'] = 'Add level';
+$string['scorepostfix'] = ' points';
+$string['leveldelete'] = 'Delete level';
+
+$string['criterionempty'] = 'Click to edit criterion';
+$string['levelempty'] = 'Click to edit level';
+
+$string['rubric'] = 'Rubric';
+$string['rubricoptions'] = 'Rubric options';
+
+$string['sortlevelsasc'] = 'Sort order for levels:';
+$string['sortlevelsasc1'] = 'Ascending by number of points';
+$string['sortlevelsasc0'] = 'Descending by number of points';
+$string['showdescriptionteacher'] = 'Display rubric description during evaluation';
+$string['showdescriptionstudent'] = 'Display rubric description to those being graded';
+$string['showscoreteacher'] = 'Display points for each level during evaluation';
+$string['showscorestudent'] = 'Display points for each level to those being graded';
+$string['enableremarks'] = 'Allow grader to add text remarks for each criteria';
+$string['showremarksstudent'] = 'Show remarks to those being graded';
diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php
new file mode 100644 (file)
index 0000000..63e08ac
--- /dev/null
@@ -0,0 +1,602 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Grading method controller for the Rubric plugin
+ *
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+
+/**
+ * This controller encapsulates the rubric grading logic
+ */
+class gradingform_rubric_controller extends gradingform_controller {
+    // Modes of displaying the rubric (used in gradingform_rubric_renderer)
+    const DISPLAY_EDIT_FULL     = 1; // For editing (moderator or teacher creates a rubric)
+    const DISPLAY_EDIT_FROZEN   = 2; // Preview the rubric design with hidden fields
+    const DISPLAY_PREVIEW       = 3; // Preview the rubric design
+    const DISPLAY_EVAL          = 4; // For evaluation, enabled (teacher grades a student)
+    const DISPLAY_EVAL_FROZEN   = 5; // For evaluation, with hidden fields
+    const DISPLAY_REVIEW        = 6; // Teacher reviews filled rubric
+    const DISPLAY_VIEW          = 7; // Dispaly filled rubric (i.e. students see their grades)
+
+    /**
+     * Extends the module settings navigation with the rubric grading settings
+     *
+     * This function is called when the context for the page is an activity module with the
+     * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms
+     * and there is an area with the active grading method set to 'rubric'.
+     *
+     * @param settings_navigation $settingsnav {@link settings_navigation}
+     * @param navigation_node $node {@link navigation_node}
+     */
+    public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) {
+        $node->add(get_string('definerubric', 'gradingform_rubric'),
+            $this->get_editor_url(), settings_navigation::TYPE_CUSTOM,
+            null, null, new pix_icon('icon', '', 'gradingform_rubric'));
+    }
+
+    /**
+     * Saves the rubric definition into the database
+     *
+     * @see parent::update_definition()
+     * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data()
+     * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
+     */
+    public function update_definition(stdClass $newdefinition, $usermodified = null) {
+        global $DB;
+
+        // firstly update the common definition data in the {grading_definition} table
+        if ($this->definition === false) {
+            // if definition does not exist yet, create a blank one with only required fields set
+            // (we need id to save files embedded in description)
+            parent::update_definition((object)array('descriptionformat' => FORMAT_MOODLE), $usermodified);
+            parent::load_definition();
+        }
+        if (!isset($newdefinition->rubric['options'])) {
+            $newdefinition->rubric['options'] = self::get_default_options();
+        }
+        $newdefinition->options = json_encode($newdefinition->rubric['options']);
+        $editoroptions = self::description_form_field_options($this->get_context());
+        $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(),
+            'gradingform_rubric', 'definition_description', $this->definition->id);
+        parent::update_definition($newdefinition, $usermodified);
+
+        // reload the definition from the database
+        $currentdefinition = $this->get_definition(true);
+
+        // update rubric data
+        $haschanges = false;
+        if (empty($newdefinition->rubric['criteria'])) {
+            $newcriteria = array();
+        } else {
+            $newcriteria = $newdefinition->rubric['criteria']; // new ones to be saved
+        }
+        $currentcriteria = $currentdefinition->rubric_criteria;
+        $criteriafields = array('sortorder', 'description', 'descriptionformat');
+        $levelfields = array('score', 'definition', 'definitionformat');
+        foreach ($newcriteria as $id => $criterion) {
+            // get list of submitted levels
+            $levelsdata = array();
+            if (array_key_exists('levels', $criterion)) {
+                $levelsdata = $criterion['levels'];
+            }
+            if (preg_match('/^NEWID\d+$/', $id)) {
+                // insert criterion into DB
+                $data = array('formid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO format is not supported yet
+                foreach ($criteriafields as $key) {
+                    if (array_key_exists($key, $criterion)) {
+                        $data[$key] = $criterion[$key];
+                    }
+                }
+                $id = $DB->insert_record('gradingform_rubric_criteria', $data);
+                $haschanges = true;
+            } else {
+                // update criterion in DB
+                $data = array();
+                foreach ($criteriafields as $key) {
+                    if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) {
+                        $data[$key] = $criterion[$key];
+                    }
+                }
+                if (!empty($data)) {
+                    // update only if something is changed
+                    $data['id'] = $id;
+                    $DB->update_record('gradingform_rubric_criteria', $data);
+                    $haschanges = true;
+                }
+                // remove deleted levels from DB
+                foreach (array_keys($currentcriteria[$id]['levels']) as $levelid) {
+                    if (!array_key_exists($levelid, $levelsdata)) {
+                        $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid));
+                        $haschanges = true;
+                    }
+                }
+            }
+            foreach ($levelsdata as $levelid => $level) {
+                if (preg_match('/^NEWID\d+$/', $levelid)) {
+                    // insert level into DB
+                    $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO format is not supported yet
+                    foreach ($levelfields as $key) {
+                        if (array_key_exists($key, $level)) {
+                            $data[$key] = $level[$key];
+                        }
+                    }
+                    $levelid = $DB->insert_record('gradingform_rubric_levels', $data);
+                    $haschanges = true;
+                } else {
+                    // update level in DB
+                    $data = array();
+                    foreach ($levelfields as $key) {
+                        if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) {
+                            $data[$key] = $level[$key];
+                        }
+                    }
+                    if (!empty($data)) {
+                        // update only if something is changed
+                        $data['id'] = $levelid;
+                        $DB->update_record('gradingform_rubric_levels', $data);
+                        $haschanges = true;
+                    }
+                }
+            }
+        }
+        // remove deleted criteria from DB
+        foreach (array_keys($currentcriteria) as $id) {
+            if (!array_key_exists($id, $newcriteria)) {
+                $DB->delete_records('gradingform_rubric_criteria', array('id' => $id));
+                $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id));
+                $haschanges = true;
+            }
+        }
+        $this->load_definition();
+    }
+
+    /**
+     * Loads the rubric form definition if it exists
+     *
+     * There is a new array called 'rubric_criteria' appended to the list of parent's definition properties.
+     */
+    protected function load_definition() {
+        global $DB;
+        $sql = "SELECT gd.*,
+                       rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat,
+                       rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat
+                  FROM {grading_definitions} gd
+             LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.formid = gd.id)
+             LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)
+                 WHERE gd.areaid = :areaid AND gd.method = :method
+              ORDER BY rc.sortorder,rl.score";
+        $params = array('areaid' => $this->areaid, 'method' => $this->get_method_name());
+
+        $rs = $DB->get_recordset_sql($sql, $params);
+        $this->definition = false;
+        foreach ($rs as $record) {
+            // pick the common definition data
+            if ($this->definition === false) {
+                $this->definition = new stdClass();
+                foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid',
+                        'timecreated', 'usercreated', 'timemodified', 'usermodified', 'options') as $fieldname) {
+                    $this->definition->$fieldname = $record->$fieldname;
+                }
+                $this->definition->rubric_criteria = array();
+            }
+            // pick the criterion data
+            if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) {
+                foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) {
+                    $this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname};
+                }
+                $this->definition->rubric_criteria[$record->rcid]['levels'] = array();
+            }
+            // pick the level data
+            if (!empty($record->rlid)) {
+                foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) {
+                    $this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $record->{'rl'.$fieldname};
+                }
+            }
+        }
+        $rs->close();
+        $options = $this->get_options();
+        if (!$options['sortlevelsasc']) {
+            foreach (array_keys($this->definition->rubric_criteria) as $rcid) {
+                $this->definition->rubric_criteria[$rcid]['levels'] = array_reverse($this->definition->rubric_criteria[$rcid]['levels'], true);
+            }
+        }
+    }
+
+    public static function get_default_options() {
+        $options = array(
+            'sortlevelsasc' => 1,
+            //'showdescriptionteacher' => 1,
+            //'showdescriptionstudent' => 1,
+            'showscoreteacher' => 1,
+            'showscorestudent' => 1,
+            'enableremarks' => 1,
+            'showremarksstudent' => 1
+        );
+        // TODO description options
+        return $options;
+    }
+
+    public function get_options() {
+        $options = self::get_default_options();
+        if (!empty($this->definition->options)) {
+            $thisoptions = json_decode($this->definition->options);
+            foreach ($thisoptions as $option => $value) {
+                $options[$option] = $value;
+            }
+        }
+        return $options;
+    }
+
+    /**
+     * Converts the current definition into an object suitable for the editor form's set_data()
+     *
+     * @return stdClass
+     */
+    public function get_definition_for_editing() {
+
+        $definition = $this->get_definition();
+        $properties = new stdClass();
+        $properties->areaid = $this->areaid;
+        if ($definition) {
+            foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) {
+                $properties->$key = $definition->$key;
+            }
+            $options = self::description_form_field_options($this->get_context());
+            $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(),
+                'gradingform_rubric', 'definition_description', $definition->id);
+        }
+        $properties->rubric = array('criteria' => array(), 'options' => $this->get_options());
+        if (!empty($definition->rubric_criteria)) {
+            $properties->rubric['criteria'] = $definition->rubric_criteria;
+        }
+
+        return $properties;
+    }
+
+    /**
+     * Returns the form definition suitable for cloning into another area
+     *
+     * @see parent::get_definition_copy()
+     * @param gradingform_controller $target the controller of the new copy
+     * @return stdClass definition structure to pass to the target's {@link update_definition()}
+     */
+    public function get_definition_copy(gradingform_controller $target) {
+
+        $new = parent::get_definition_copy($target);
+        $old = $this->get_definition_for_editing();
+        $new->description_editor = $old->description_editor;
+        $new->rubric = array('criteria' => array(), 'options' => $old->rubric['options']);
+        $newcritid = 1;
+        $newlevid = 1;
+        foreach ($old->rubric['criteria'] as $oldcritid => $oldcrit) {
+            unset($oldcrit['id']);
+            if (isset($oldcrit['levels'])) {
+                foreach ($oldcrit['levels'] as $oldlevid => $oldlev) {
+                    unset($oldlev['id']);
+                    $oldcrit['levels']['NEWID'.$newlevid] = $oldlev;
+                    unset($oldcrit['levels'][$oldlevid]);
+                    $newlevid++;
+                }
+            } else {
+                $oldcrit['levels'] = array();
+            }
+            $new->rubric['criteria']['NEWID'.$newcritid] = $oldcrit;
+            $newcritid++;
+        }
+
+        return $new;
+    }
+
+    // TODO the following functions may be moved to parent:
+
+    /**
+     * @return array options for the form description field
+     */
+    public static function description_form_field_options($context) {
+        global $CFG;
+        return array(
+            'maxfiles' => -1,
+            'maxbytes' => get_max_upload_file_size($CFG->maxbytes),
+            'context'  => $context,
+        );
+    }
+
+    public function get_formatted_description() {
+        if ($this->definition === false) {
+            return null;
+        }
+        $context = $this->get_context();
+
+        $options = self::description_form_field_options($this->get_context());
+        $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id,
+            'gradingform_rubric', 'definition_description', $this->definition->id, $options);
+
+        $formatoptions = array(
+            'noclean' => false,
+            'trusted' => false,
+            'filter' => true,
+            'context' => $context
+        );
+        return format_text($description, $this->definition->descriptionformat, $formatoptions);
+    }
+
+    public function is_form_available($foruserid = null) {
+        return true;
+        // TODO this is temporary for testing!
+    }
+
+    /**
+     * Returns the rubric plugin renderer
+     *
+     * @param moodle_page $page the target page
+     * @return renderer_base
+     */
+    public function get_renderer(moodle_page $page) {
+        return $page->get_renderer('gradingform_'. $this->get_method_name());
+    }
+
+    /**
+     * Returns the HTML code displaying the preview of the grading form
+     *
+     * @param moodle_page $page the target page
+     * @return string
+     */
+    public function render_preview(moodle_page $page) {
+
+        // use the parent's method to render the common information about the form
+        $header = parent::render_preview($page);
+
+        // append the rubric itself, using own renderer
+        $output = $this->get_renderer($page);
+        $criteria = $this->definition->rubric_criteria;
+        $options = $this->get_options();
+        $rubric = $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
+
+        return $header . $rubric;
+    }
+
+    /**
+     * Deletes the rubric definition and all the associated information
+     */
+    protected function delete_plugin_definition() {
+        global $DB;
+
+        // get the list of instances
+        $instances = array_keys($DB->get_records('grading_instances', array('formid' => $this->definition->id), '', 'id'));
+        // delete all fillings
+        $DB->delete_records_list('gradingform_rubric_fillings', 'forminstanceid', $instances);
+        // delete instances
+        $DB->delete_records_list('grading_instances', 'id', $instances);
+        // get the list of criteria records
+        $criteria = array_keys($DB->get_records('gradingform_rubric_criteria', array('formid' => $this->definition->id), '', 'id'));
+        // delete levels
+        $DB->delete_records_list('gradingform_rubric_levels', 'criterionid', $criteria);
+        // delete critera
+        $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria);
+    }
+
+    /**
+     * Returns html code to be included in student's feedback.
+     *
+     * @param moodle_page $page
+     * @param int $itemid
+     * @param array $grading_info result of function grade_get_grades
+     * @param string $defaultcontent default string to be returned if no active grading is found
+     * @return string
+     */
+    public function render_grade($page, $itemid, $grading_info, $defaultcontent) {
+        $instances = $this->get_current_instances($itemid);
+        return $this->get_renderer($page)->display_instances($this->get_current_instances($itemid), $defaultcontent);
+    }
+}
+
+/**
+ * Class to manage one rubric grading instance. Stores information and performs actions like
+ * update, copy, validate, submit, etc.
+ *
+ * @copyright  2011 Marina Glancy
+ */
+class gradingform_rubric_instance extends gradingform_instance {
+
+    protected $rubric;
+
+    /**
+     * Deletes this (INCOMPLETE) instance from database.
+     */
+    public function cancel() {
+        global $DB;
+        parent::cancel();
+        $DB->delete_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id()));
+    }
+
+    /**
+     * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with
+     * the specified values)
+     *
+     * @param int $raterid value for raterid in the duplicate
+     * @param int $itemid value for itemid in the duplicate
+     * @return int id of the new instance
+     */
+    public function copy($raterid, $itemid) {
+        global $DB;
+        $instanceid = parent::copy($raterid, $itemid);
+        $currentgrade = $this->get_rubric_filling();
+        foreach ($currentgrade['criteria'] as $criterionid => $record) {
+            $params = array('forminstanceid' => $instanceid, 'criterionid' => $criterionid,
+                'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']);
+            $DB->insert_record('gradingform_rubric_fillings', $params);
+        }
+        return $instanceid;
+    }
+
+    /**
+     * Validates that rubric is fully completed and contains valid grade on each criterion
+     * @return boolean true if the form data is validated and contains no errors
+     */
+    public function validate_grading_element($elementvalue) {
+        // TODO: if there is nothing selected in rubric, we don't enter this function at all :(
+        $criteria = $this->get_controller()->get_definition()->rubric_criteria;
+        if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) {
+            return false;
+        }
+        foreach ($criteria as $id => $criterion) {
+            if (!isset($elementvalue['criteria'][$id]['levelid'])
+                    || !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Retrieves from DB and returns the data how this rubric was filled
+     *
+     * @param boolean $force whether to force DB query even if the data is cached
+     * @return array
+     */
+    public function get_rubric_filling($force = false) {
+        global $DB;
+        if ($this->rubric === null || $force) {
+            $records = $DB->get_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id()));
+            $this->rubric = array('criteria' => array());
+            foreach ($records as $record) {
+                $this->rubric['criteria'][$record->criterionid] = (array)$record;
+            }
+        }
+        return $this->rubric;
+    }
+
+    /**
+     * Updates the instance with the data received from grading form. This function may be
+     * called via AJAX when grading is not yet completed, so it does not change the
+     * status of the instance.
+     *
+     * @param array $data
+     */
+    public function update($data) {
+        global $DB;
+        $currentgrade = $this->get_rubric_filling();
+        parent::update($data);
+        foreach ($data['criteria'] as $criterionid => $record) {
+            if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
+                $newrecord = array('forminstanceid' => $this->get_id(), 'criterionid' => $criterionid,
+                    'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => FORMAT_MOODLE);
+                $DB->insert_record('gradingform_rubric_fillings', $newrecord);
+            } else {
+                $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
+                foreach (array('levelid', 'remark'/*, 'remarkformat' TODO */) as $key) {
+                    if ($currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
+                        $newrecord[$key] = $record[$key];
+                    }
+                }
+                if (count($newrecord) > 1) {
+                    $DB->update_record('gradingform_rubric_fillings', $newrecord);
+                }
+            }
+        }
+        foreach ($currentgrade['criteria'] as $criterionid => $record) {
+            if (!array_key_exists($criterionid, $data['criteria'])) {
+                $DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id']));
+            }
+        }
+        $this->get_rubric_filling(true);
+    }
+
+    /**
+     * Calculates the grade to be pushed to the gradebook
+     *
+     * @return int the valid grade from $this->get_controller()->get_grade_range()
+     */
+    public function get_grade() {
+        global $DB, $USER;
+        $grade = $this->get_rubric_filling();
+
+        $minscore = 0;
+        $maxscore = 0;
+        foreach ($this->get_controller()->get_definition()->rubric_criteria as $id => $criterion) {
+            $keys = array_keys($criterion['levels']);
+            sort($keys);
+            $minscore += $criterion['levels'][$keys[0]]['score'];
+            $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score'];
+        }
+
+        if ($maxscore <= $minscore) {
+            return -1;
+        }
+
+        $graderange = array_keys($this->get_controller()->get_grade_range());
+        if (empty($graderange)) {
+            return -1;
+        }
+        sort($graderange);
+        $mingrade = $graderange[0];
+        $maxgrade = $graderange[sizeof($graderange) - 1];
+
+        $curscore = 0;
+        foreach ($grade['criteria'] as $id => $record) {
+            $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
+        }
+        return round(($curscore-$minscore)/($maxscore-$minscore)*($maxgrade-$mingrade), 0) + $mingrade; // TODO mapping
+    }
+
+    /**
+     * Returns the error message displayed in case of validation failed
+     *
+     * @return string
+     */
+    public function default_validation_error_message() {
+        return 'The rubric is incomplete'; //TODO string
+    }
+
+    /**
+     * Returns html for form element of type 'grading'.
+     *
+     * @param moodle_page $page
+     * @param MoodleQuickForm_grading $formelement
+     * @return string
+     */
+    public function render_grading_element($page, $gradingformelement) {
+        global $USER;
+        if (!$gradingformelement->_flagFrozen) {
+            $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
+            $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module);
+            $mode = gradingform_rubric_controller::DISPLAY_EVAL;
+        } else {
+            if ($gradingformelement->_persistantFreeze) {
+                $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN;
+            } else {
+                $mode = gradingform_rubric_controller::DISPLAY_REVIEW;
+            }
+        }
+        $criteria = $this->get_controller()->get_definition()->rubric_criteria;
+        $options = $this->get_controller()->get_options();
+        $value = $gradingformelement->getValue();
+        if ($value === null) {
+            $value = $this->get_rubric_filling();
+        }
+        return $this->get_controller()->get_renderer($page)->display_rubric($criteria, $options, $mode, $gradingformelement->getName(), $value);
+    }
+}
\ No newline at end of file
diff --git a/grade/grading/form/rubric/pix/icon.png b/grade/grading/form/rubric/pix/icon.png
new file mode 100644 (file)
index 0000000..f1af82f
Binary files /dev/null and b/grade/grading/form/rubric/pix/icon.png differ
diff --git a/grade/grading/form/rubric/renderer.php b/grade/grading/form/rubric/renderer.php
new file mode 100644 (file)
index 0000000..4899778
--- /dev/null
@@ -0,0 +1,406 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grading method plugin renderer
+ */
+class gradingform_rubric_renderer {
+
+    /**
+     * This function returns html code for displaying criterion. Depending on $mode it may be the
+     * code to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation.
+     *
+     * This function may be called from display_rubric() to display the whole rubric, or it can be
+     * called by itself to return a template used by JavaScript to add new empty criteria to the
+     * rubric being designed.
+     * In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc.
+     *
+     * When overriding this function it is very important to remember that all elements of html
+     * form (in edit or evaluate mode) must have the name $elementname.
+     *
+     * Also JavaScript relies on the class names of elements and when developer changes them
+     * script might stop working.
+     *
+     * @param int $mode rubric display mode @see gradingform_rubric_controller
+     * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
+     * @param array|null $criterion criterion data
+     * @param string $levels_str evaluated templates for this criterion levels
+     * @param array|null $value (only in view mode) teacher's feedback on this criterion
+     * @return string
+     */
+    public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $levels_str = '{LEVELS}', $value = null) {
+        // TODO description format, remark format
+        if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
+            $criterion = array('id' => '{CRITERION-id}', 'description' => '{CRITERION-description}', 'sortorder' => '{CRITERION-sortorder}', 'class' => '{CRITERION-class}');
+        } else {
+            foreach (array('sortorder', 'description', 'class') as $key) {
+                // set missing array elements to empty strings to avoid warnings
+                if (!array_key_exists($key, $criterion)) {
+                    $criterion[$key] = '';
+                }
+            }
+        }
+        $criterion_template = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], 'id' => '{NAME}-criteria-{CRITERION-id}'));
+        if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $criterion_template .= html_writer::start_tag('td', array('class' => 'controls'));
+            foreach (array('moveup', 'delete', 'movedown') as $key) {
+                $value = get_string('criterion'.$key, 'gradingform_rubric');
+                $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
+                    'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value));
+                $criterion_template .= html_writer::tag('div', $button, array('class' => $key));
+            }
+            $criterion_template .= html_writer::end_tag('td'); // .controls
+            $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
+            $description = html_writer::tag('textarea', htmlspecialchars($criterion['description']), array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5'));
+        } else {
+            if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+                $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
+                $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description']));
+            }
+            $description = $criterion['description'];
+        }
+        $criterion_template .= html_writer::tag('td', $description, array('class' => 'description', 'id' => '{NAME}-criteria-{CRITERION-id}-description'));
+        $levels_str_table = html_writer::tag('table', html_writer::tag('tr', $levels_str, array('id' => '{NAME}-criteria-{CRITERION-id}-levels')));
+        $criterion_template .= html_writer::tag('td', $levels_str_table, array('class' => 'levels'));
+        if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $value = get_string('criterionaddlevel', 'gradingform_rubric');
+            $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][addlevel]',
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value, 'title' => $value));
+            $criterion_template .= html_writer::tag('td', $button, array('class' => 'addlevel'));
+        }
+        $displayremark = ($options['enableremarks'] && ($mode != gradingform_rubric_controller::DISPLAY_VIEW || $options['showremarksstudent']));
+        if ($displayremark) {
+            $currentremark = '';
+            if (isset($value['remark'])) {
+                $currentremark = $value['remark'];
+            }
+            if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
+                $input = html_writer::tag('textarea', htmlspecialchars($currentremark), array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '10', 'rows' => '5'));
+                $criterion_template .= html_writer::tag('td', $input, array('class' => 'remark'));
+            } else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
+                $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
+            }else if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW) {
+                $criterion_template .= html_writer::tag('td', $currentremark, array('class' => 'remark')); // TODO maybe some prefix here like 'Teacher remark:'
+            }
+        }
+        $criterion_template .= html_writer::end_tag('tr'); // .criterion
+
+        $criterion_template = str_replace('{NAME}', $elementname, $criterion_template);
+        $criterion_template = str_replace('{CRITERION-id}', $criterion['id'], $criterion_template);
+        return $criterion_template;
+    }
+
+    /**
+     * This function returns html code for displaying one level of one criterion. Depending on $mode
+     * it may be the code to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation.
+     *
+     * This function may be called from display_rubric() to display the whole rubric, or it can be
+     * called by itself to return a template used by JavaScript to add new empty level to the
+     * criterion during the design of rubric.
+     * In this case it will use macros like {NAME}, {CRITERION-id}, {LEVEL-id}, etc.
+     *
+     * When overriding this function it is very important to remember that all elements of html
+     * form (in edit or evaluate mode) must have the name $elementname.
+     *
+     * Also JavaScript relies on the class names of elements and when developer changes them
+     * script might stop working.
+     *
+     * @param int $mode rubric display mode @see gradingform_rubric_controller
+     * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
+     * @param string|int $criterionid either id of the nesting criterion or a macro for template
+     * @param array|null $level level data, also in view mode it might also have property $level['checked'] whether this level is checked
+     * @return string
+     */
+    public function level_template($mode, $options, $elementname = '{NAME}', $criterionid = '{CRITERION-id}', $level = null) {
+        // TODO definition format
+        if (!isset($level['id'])) {
+            $level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false);
+        } else {
+            foreach (array('score', 'definition', 'class', 'checked') as $key) {
+                // set missing array elements to empty strings to avoid warnings
+                if (!array_key_exists($key, $level)) {
+                    $level[$key] = '';
+                }
+            }
+        }
+
+        // Template for one level within one criterion
+        $tdattributes = array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}', 'class' => 'level'. $level['class']);
+        if (isset($level['tdwidth'])) {
+            $tdattributes['width'] = round($level['tdwidth']).'%';
+        }
+        $level_template = html_writer::start_tag('td', $tdattributes);
+        $level_template .= html_writer::start_tag('div', array('class' => 'level-wrapper'));
+        if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $definition = html_writer::tag('textarea', htmlspecialchars($level['definition']), array('name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4'));
+            $score = html_writer::empty_tag('input', array('type' => 'text', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '4', 'value' => $level['score']));
+        } else {
+            if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+                $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
+                $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'value' => $level['score']));
+            }
+            $definition = $level['definition'];
+            $score = $level['score'];
+        }
+        if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
+            $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']) +
+                    ($level['checked'] ? array('checked' => 'checked') : array()));
+            $level_template .= html_writer::tag('div', $input, array('class' => 'radio'));
+        }
+        if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
+            $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']));
+        }
+        $score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score'));
+        $level_template .= html_writer::tag('div', $definition, array('class' => 'definition', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition'));
+        $displayscore = true;
+        if (!$options['showscoreteacher'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_EVAL, gradingform_rubric_controller::DISPLAY_EVAL_FROZEN, gradingform_rubric_controller::DISPLAY_REVIEW))) {
+            $displayscore = false;
+        }
+        if (!$options['showscorestudent'] && $mode == gradingform_rubric_controller::DISPLAY_VIEW) {
+            $displayscore = false;
+        }
+        if ($displayscore) {
+            $level_template .= html_writer::tag('div', $score. get_string('scorepostfix', 'gradingform_rubric'), array('class' => 'score'));
+        }
+        if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $value = get_string('leveldelete', 'gradingform_rubric');
+            $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]', 'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete', 'value' => $value, 'title' => $value));
+            $level_template .= html_writer::tag('div', $button, array('class' => 'delete'));
+        }
+        $level_template .= html_writer::end_tag('div'); // .level-wrapper
+        $level_template .= html_writer::end_tag('td'); // .level
+
+        $level_template = str_replace('{NAME}', $elementname, $level_template);
+        $level_template = str_replace('{CRITERION-id}', $criterionid, $level_template);
+        $level_template = str_replace('{LEVEL-id}', $level['id'], $level_template);
+        return $level_template;
+    }
+
+    /**
+     * This function returns html code for displaying rubric template (content before and after
+     * criteria list). Depending on $mode it may be the code to edit rubric, to preview the rubric,
+     * to evaluate somebody or to review the evaluation.
+     *
+     * This function is called from display_rubric() to display the whole rubric.
+     *
+     * When overriding this function it is very important to remember that all elements of html
+     * form (in edit or evaluate mode) must have the name $elementname.
+     *
+     * Also JavaScript relies on the class names of elements and when developer changes them
+     * script might stop working.
+     *
+     * @param int $mode rubric display mode @see gradingform_rubric_controller
+     * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
+     * @param string $criteria_str evaluated templates for this rubric's criteria
+     * @return string
+     */
+    protected function rubric_template($mode, $options, $elementname, $criteria_str) {
+        $classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode
+        switch ($mode) {
+            case gradingform_rubric_controller::DISPLAY_EDIT_FULL:
+                $classsuffix = ' editor editable'; break;
+            case gradingform_rubric_controller::DISPLAY_EDIT_FROZEN:
+                $classsuffix = ' editor frozen';  break;
+            case gradingform_rubric_controller::DISPLAY_PREVIEW:
+                $classsuffix = ' editor preview';  break;
+            case gradingform_rubric_controller::DISPLAY_EVAL:
+                $classsuffix = ' evaluate editable'; break;
+            case gradingform_rubric_controller::DISPLAY_EVAL_FROZEN:
+                $classsuffix = ' evaluate frozen';  break;
+            case gradingform_rubric_controller::DISPLAY_REVIEW:
+                $classsuffix = ' review';  break;
+            case gradingform_rubric_controller::DISPLAY_VIEW:
+                $classsuffix = ' view';  break;
+        }
+
+        $rubric_template = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix gradingform_rubric'.$classsuffix));
+        $rubric_template .= html_writer::tag('table', $criteria_str, array('class' => 'criteria', 'id' => '{NAME}-criteria'));
+        if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+            $value = get_string('addcriterion', 'gradingform_rubric');
+            $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]', 'id' => '{NAME}-criteria-addcriterion', 'value' => $value, 'title' => $value));
+            $rubric_template .= html_writer::tag('div', $input, array('class' => 'addcriterion'));
+        }
+        $rubric_template .= $this->rubric_edit_options($mode, $options);
+        $rubric_template .= html_writer::end_tag('div');
+
+        return str_replace('{NAME}', $elementname, $rubric_template);
+    }
+
+    protected function rubric_edit_options($mode, $options) {
+        if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL
+                && $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN
+                && $mode != gradingform_rubric_controller::DISPLAY_PREVIEW) {
+            // Options are displayed only in edit mode
+            return;
+        }
+        $html = html_writer::start_tag('div', array('class' => 'options'));
+        $html .= html_writer::tag('div', get_string('rubricoptions', 'gradingform_rubric'), array('class' => 'optionsheading'));
+        $attrs = array('type' => 'hidden', 'name' => '{NAME}[options][optionsset]', 'value' => 1);
+        foreach ($options as $option => $value) {
+            $html .= html_writer::start_tag('div', array('class' => 'option '.$option));
+            $attrs = array('name' => '{NAME}[options]['.$option.']', 'id' => '{NAME}-options-'.$option);
+            switch ($option) {
+                case 'sortlevelsasc':
+                    // Display option as dropdown
+                    $html .= html_writer::tag('span', get_string($option, 'gradingform_rubric'), array('class' => 'label'));
+                    $value = (int)(!!$value); // make sure $value is either 0 or 1
+                    if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
+                        $selectoptions = array(0 => get_string($option.'0', 'gradingform_rubric'), 1 => get_string($option.'1', 'gradingform_rubric'));
+                        $value_str = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id']));
+                        $html .= html_writer::tag('span', $value_str, array('class' => 'value'));
+                        // TODO add here button 'Sort levels'
+                    } else {
+                        $html .= html_writer::tag('span', get_string($option.$value, 'gradingform_rubric'), array('class' => 'value'));
+                        if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+                            $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value));
+                        }
+                    }
+                    break;
+                default:
+                    // Display option as checkbox
+                    $attrs['type'] = 'checkbox';
+                    $attrs['value'] = 1;
+                    if ($value) {
+                        $attrs['checked'] = 'checked';
+                    }
+                    if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
+                        $attrs['disabled'] = 'disabled';
+                    }
+                    if ($mode == gradingform_rubric_controller::DISPLAY_PREVIEW) {
+                        $attrs['disabled'] = 'disabled';
+                        unset($attrs['name']);
+                    }
+                    $html .= html_writer::empty_tag('input', $attrs);
+                    $html .= html_writer::tag('label', get_string($option, 'gradingform_rubric'), array('for' => $attrs['id']));
+                    break;
+            }
+            $html .= html_writer::end_tag('div'); // .option
+        }
+        $html .= html_writer::end_tag('div'); // .options
+        return $html;
+    }
+
+    /**
+     * This function returns html code for displaying rubric. Depending on $mode it may be the code
+     * to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation.
+     *
+     * It is very unlikely that this function needs to be overriden by theme. It does not produce
+     * any html code, it just prepares data about rubric design and evaluation, adds the CSS
+     * class to elements and calls the functions level_template, criterion_template and
+     * rubric_template
+     *
+     * @param array $criteria data about the rubric design
+     * @param int $mode rubric display mode @see gradingform_rubric_controller
+     * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
+     * @param array $values evaluation result
+     * @return string
+     */
+    public function display_rubric($criteria, $options, $mode, $elementname = null, $values = null) {
+        $criteria_str = '';
+        $cnt = 0;
+        foreach ($criteria as $id => $criterion) {
+            $criterion['class'] = $this->get_css_class_suffix($cnt++, sizeof($criteria) -1);
+            $criterion['id'] = $id;
+            $levels_str = '';
+            $levelcnt = 0;
+            if (isset($values['criteria'][$id])) {
+                $criterionvalue = $values['criteria'][$id];
+            } else {
+                $criterionvalue = null;
+            }
+            foreach ($criterion['levels'] as $levelid => $level) {
+                $level['id'] = $levelid;
+                $level['score'] = (float)$level['score']; // otherwise the display will look like 1.00000
+                $level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1);
+                $level['checked'] = (isset($criterionvalue['levelid']) && ((int)$criterionvalue['levelid'] === $levelid));
+                if ($level['checked'] && ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW)) {
+                    $level['class'] .= ' checked';
+                    //in mode DISPLAY_EVAL the class 'checked' will be added by JS if it is enabled. If JS is not enabled, the 'checked' class will only confuse
+                }
+                $level['tdwidth'] = 100/count($criterion['levels']);
+                $levels_str .= $this->level_template($mode, $options, $elementname, $id, $level);
+            }
+            $criteria_str .= $this->criterion_template($mode, $options, $elementname, $criterion, $levels_str, $criterionvalue);
+        }
+        return $this->rubric_template($mode, $options, $elementname, $criteria_str);
+    }
+
+    /**
+     * Help function to return CSS class names for element (first/last/even/odd) with leading space
+     *
+     * @param int $cnt
+     * @param int $maxcnt
+     * @return string
+     */
+    protected function get_css_class_suffix($cnt, $maxcnt) {
+        $class = '';
+        if ($cnt == 0) {
+            $class .= ' first';
+        }
+        if ($cnt == $maxcnt) {
+            $class .= ' last';
+        }
+        if ($cnt%2) {
+            $class .= ' odd';
+        } else {
+            $class .= ' even';
+        }
+        return $class;
+    }
+
+    /**
+     * Displays for the student the list of instances or default content if no instances found
+     *
+     * @param array $instances array of objects of type gradingform_rubric_instance
+     * @param string $defaultcontent default string that would be displayed without advanced grading
+     * @return string
+     */
+    public function display_instances($instances, $defaultcontent) {
+        if (sizeof($instances)) {
+            $rv = html_writer::start_tag('div', array('class' => 'advancedgrade'));
+            $idx = 0;
+            foreach ($instances as $instance) {
+                $rv .= $this->display_instance($instance, $idx++);
+            }
+            $rv .= html_writer::end_tag('div');
+        }
+        return $rv. $defaultcontent;
+    }
+
+    /**
+     * Displays one grading instance
+     *
+     * @param gradingform_rubric_instance $instance
+     * @param int idx unique number of instance on page
+     */
+    public function display_instance(gradingform_rubric_instance $instance, $idx) {
+        $criteria = $instance->get_controller()->get_definition()->rubric_criteria;
+        $options = $instance->get_controller()->get_options();
+        $values = $instance->get_rubric_filling();
+        // TODO mode should be DISPLAY_REVIEW if this user is a teacher
+        return $this->display_rubric($criteria, $options, gradingform_rubric_controller::DISPLAY_VIEW, 'rubric'.$idx, $values);
+    }
+}
diff --git a/grade/grading/form/rubric/rubriceditor.php b/grade/grading/form/rubric/rubriceditor.php
new file mode 100644 (file)
index 0000000..5146149
--- /dev/null
@@ -0,0 +1,215 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("HTML/QuickForm/input.php");
+
+// register file-related rules
+if (class_exists('HTML_QuickForm')) {
+    HTML_QuickForm::registerRule('rubriceditorcompleted', 'callback', '_ruleIsCompleted', 'MoodleQuickForm_rubriceditor');
+}
+
+class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
+    public $_helpbutton = '';
+
+    function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) {
+        parent::HTML_QuickForm_input($elementName, $elementLabel, $attributes);
+    }
+
+    function getHelpButton() {
+        return $this->_helpbutton;
+    }
+
+    function getElementTemplateType() {
+        return 'default';
+    }
+
+    function toHtml() {
+        global $PAGE;
+        $html = $this->_getTabs();
+        $renderer = $PAGE->get_renderer('gradingform_rubric');
+        $data = $this->prepare_non_js_data();
+        if (!$this->_flagFrozen) {
+            $mode = gradingform_rubric_controller::DISPLAY_EDIT_FULL;
+            $module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js',
+                'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
+                    array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
+                    ));
+            $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(
+                array('name' => $this->getName(),
+                    'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
+                    'leveltemplate' => $renderer->level_template($mode, $data['options'], $this->getName())
+                   )),
+                true, $module);
+        } else {
+            // Rubric is frozen, no javascript needed
+            if ($this->_persistantFreeze) {
+                $mode = gradingform_rubric_controller::DISPLAY_EDIT_FROZEN;
+            } else {
+                $mode = gradingform_rubric_controller::DISPLAY_PREVIEW;
+            }
+        }
+        $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
+        return $html;
+    }
+
+    /**
+     * Prepares the data passed in $_POST:
+     * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
+     * - if options not passed (i.e. we create a new rubric) fills the options array with the default values
+     * - if options are passed completes the options array with unchecked checkboxes
+     *
+     * @param array $value
+     * @return array
+     */
+    function prepare_non_js_data($value = null) {
+        if (null === $value) {
+            $value = $this->getValue();
+        }
+        $return = array('criteria' => array(), 'options' => gradingform_rubric_controller::get_default_options());
+        if (!isset($value['criteria'])) {
+            $value['criteria'] = array();
+        }
+        if (!empty($value['options'])) {
+            foreach (array_keys($return['options']) as $option) {
+                // special treatment for checkboxes
+                if (!empty($value['options'][$option])) {
+                    $return['options'][$option] = $value['options'][$option];
+                } else {
+                    $return['options'][$option] = null;
+                }
+            }
+        }
+        $lastaction = null;
+        $lastid = null;
+        foreach ($value['criteria'] as $id => $criterion) {
+            if ($id == 'addcriterion') {
+                $id = $this->get_next_id(array_keys($value['criteria']));
+                $criterion = array('description' => '');
+            }
+            $levels = array();
+            if (array_key_exists('levels', $criterion)) {
+                foreach ($criterion['levels'] as $levelid => $level) {
+                    if ($levelid == 'addlevel') {
+                        $levelid = $this->get_next_id(array_keys($criterion['levels']));
+                        $level = array(
+                            'definition' => '',
+                            'score' => 0,
+                        );
+                    }
+                    if (!array_key_exists('delete', $level)) {
+                        $levels[$levelid] = $level;
+                    }
+                }
+            }
+            $criterion['levels'] = $levels;
+            if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
+                unset($criterion['moveup']);
+                if ($lastid !== null) {
+                    $lastcriterion = $return['criteria'][$lastid];
+                    unset($return['criteria'][$lastid]);
+                    $return['criteria'][$id] = $criterion;
+                    $return['criteria'][$lastid] = $lastcriterion;
+                } else {
+                    $return['criteria'][$id] = $criterion;
+                }
+                $lastaction = null;
+                $lastid = $id;
+            } else if (array_key_exists('delete', $criterion)) {
+            } else {
+                if (array_key_exists('movedown', $criterion)) {
+                    unset($criterion['movedown']);
+                    $lastaction = 'movedown';
+                }
+                $return['criteria'][$id] = $criterion;
+                $lastid = $id;
+            }
+        }
+        $csortorder = 1;
+        foreach (array_keys($return['criteria']) as $id) {
+            $return['criteria'][$id]['sortorder'] = $csortorder++;
+        }
+        return $return;
+    }
+
+    function get_next_id($ids) {
+        $maxid = 0;
+        foreach ($ids as $id) {
+            if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
+                $maxid = (int)$matches[1];
+            }
+        }
+        return 'NEWID'.($maxid+1);
+    }
+
+    function _ruleIsCompleted($elementValue) {
+        //echo "_ruleIsCompleted";
+        if (isset($elementValue['criteria'])) {
+            foreach ($elementValue['criteria'] as $criterionid => $criterion) {
+                if ($criterionid == 'addcriterion') {
+                    return false;
+                }
+                if (array_key_exists('moveup', $criterion) || array_key_exists('movedown', $criterion) || array_key_exists('delete', $criterion)) {
+                    return false;
+                }
+                if (array_key_exists('levels', $criterion) && is_array($criterion['levels'])) {
+                    foreach ($criterion['levels'] as $levelid => $level) {
+                        if ($levelid == 'addlevel') {
+                            return false;
+                        }
+                        if (array_key_exists('delete', $level)) {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+        //TODO check everything is filled
+        //echo "<pre>";print_r($elementValue);echo "</pre>";
+        return true;
+    }
+
+    function onQuickFormEvent($event, $arg, &$caller)
+    {
+        $name = $this->getName();
+        if ($name && $caller->elementExists($name)) {
+            $caller->addRule($name, '', 'rubriceditorcompleted');
+        }
+        return parent::onQuickFormEvent($event, $arg, $caller);
+    }
+
+    /**
+     * Prepares the data for saving
+     * @see prepare_non_js_data
+     *
+     * @param array $submitValues
+     * @param boolean $assoc
+     * @return array
+     */
+    function exportValue(&$submitValues, $assoc = false) {
+        $value =  $this->prepare_non_js_data($this->_findValue($submitValues));
+        return $this->_prepareValue($value, $assoc);
+    }
+}
\ No newline at end of file
diff --git a/grade/grading/form/rubric/styles.css b/grade/grading/form/rubric/styles.css
new file mode 100644 (file)
index 0000000..66dc3c0
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+.gradingform_rubric.editor[.frozen|.editable]
+  .criteria
+    .criterion[.first][.last][.odd|.even]
+        .controls
+            .moveup
+                [input type=submit]
+            .delete
+                [input type=submit]
+            .movedown
+                [input type=submit]
+        .description
+        .levels
+            td.level[.first][.last][.odd|.even]
+                div.level-wrapper
+                    .definition
+                        [textarea]
+                    .score
+                        span
+                            [input type=text]
+                    .delete
+                        [input type=submit]
+        .addlevel
+            [input type=submit]
+        .remark
+            textarea
+    .addcriterion
+        [input type=submit]
+  .options
+    .optionsheading
+    .option.OPTIONNAME
+
+.gradingform_rubric[.review][.evaluate[.editable|.frozen]]
+  .criteria
+    .criterion[.first][.last][.odd|.even]
+        .description
+        .levels
+            td.level[.first][.last][.odd|.even]
+                div.level-wrapper
+                    div.radio
+                        input
+                    .definition
+                    .score
+                        span
+
+*/
+
+.gradingform_rubric.editor .criterion .controls,
+.gradingform_rubric .criterion .description,
+.gradingform_rubric .criterion .levels,
+.gradingform_rubric.editor .criterion .addlevel,
+.gradingform_rubric .criterion .remark,
+.gradingform_rubric .criterion .levels .level {vertical-align: top;}
+
+.gradingform_rubric.editor .criterion .controls,
+.gradingform_rubric .criterion .description,
+.gradingform_rubric.editor .criterion .addlevel,
+.gradingform_rubric .criterion .remark,
+.gradingform_rubric .criterion .levels .level {padding:3px;}
+
+.gradingform_rubric .criteria {height:100%;}
+.gradingform_rubric .criterion {border:1px solid #DDD;overflow: hidden;}
+.gradingform_rubric .criterion.even {background:#F0F0F0;}
+
+.gradingform_rubric .criterion .description {width:150px;font-weight:bold;}
+
+.gradingform_rubric .criterion .levels table {width:100%;height:100%;}
+.gradingform_rubric .criterion .levels,
+.gradingform_rubric .criterion .levels table,
+.gradingform_rubric .criterion .levels table tbody {padding:0;margin:0;}
+.gradingform_rubric .criterion .levels .level {border-left:1px solid #DDD;max-width:150px;}
+.gradingform_rubric .criterion .levels .level .level-wrapper {position:relative;}
+.gradingform_rubric .criterion .levels .level.last {border-right:1px solid #DDD;}
+
+.gradingform_rubric .plainvalue.empty {font-style: italic; color: #AAA;}
+
+.gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;bottom:0;}
+.gradingform_rubric .criterion .levels .level .score {font-style:italic;color:#575;font-weight: bold;margin-top:5px;}
+
+/* Make invisible the buttons 'Move up' for the first criterion and 'Move down' for the last, because those buttons will make no change */
+.gradingform_rubric.editor .criterion.first .controls .moveup input,
+.gradingform_rubric.editor .criterion.last .controls .movedown input {display:none;}
+
+/* evaluation */
+.gradingform_rubric .criterion .levels .level.checked {background:#d0ffd0;}
+.gradingform_rubric.evaluate .criterion .levels .level:hover {background:#30ff30;}
+
+/* replace buttons with images */
+.gradingform_rubric.editor .delete input,
+.gradingform_rubric.editor .moveup input,
+.gradingform_rubric.editor .movedown input{text-indent: -1000em;cursor:pointer;border:none;}
+.gradingform_rubric.editor .criterion .controls .delete input {width:20px;height:16px;background: transparent url([[pix:i/cross_red_big]]) no-repeat center top;}
+.gradingform_rubric.editor .levels .level .delete input {width:20px;height:16px;background: transparent url([[pix:i/cross_red_small]]) no-repeat center top;}
+.gradingform_rubric.editor .moveup input {width:20px;height:15px;background: transparent url([[pix:t/up]]) no-repeat center top;margin-top:4px;}
+.gradingform_rubric.editor .movedown input {width:20px;height:15px;background: transparent url([[pix:t/down]]) no-repeat center top;margin-top:4px;}
+
+.gradingform_rubric.editor .addcriterion input,
+.gradingform_rubric.editor .addlevel input {background: transparent url([[pix:t/addgreen]]) no-repeat;display:block;color:#555555;font-weight:bold;text-decoration:none;}
+.gradingform_rubric.editor .addcriterion input {background-position: 5px 8px;height:30px;line-height:29px;margin-bottom:14px;padding-left:20px;padding-right:10px;}
+.gradingform_rubric.editor .addlevel input {background-position: 5px 6px;height:25px;line-height:24px;margin-bottom:10px;padding-left:18px;padding-right:8px;}
+
+.gradingform_rubric .options .optionsheading {font-weight:bold;font-size:1.1em;padding-bottom:5px;}
+.gradingform_rubric .options .option {padding-bottom:2px;}
+.gradingform_rubric .options .option label {margin-left: 5px;}
+.gradingform_rubric .options .option .value {margin-left: 5px;font-weight:bold;}
diff --git a/grade/grading/form/rubric/version.php b/grade/grading/form/rubric/version.php
new file mode 100644 (file)
index 0000000..a893dcb
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    gradingform
+ * @subpackage rubric
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version  = 2011101401;
+$plugin->requires = 2011092900;  // Requires this Moodle version
diff --git a/grade/grading/lib.php b/grade/grading/lib.php
new file mode 100644 (file)
index 0000000..ca36655
--- /dev/null
@@ -0,0 +1,532 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Advanced grading methods support
+ *
+ * @package    core
+ * @subpackage grading
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Factory method returning an instance of the grading manager
+ *
+ * @param stdClass|int $context or $areaid
+ * @param string $component the frankenstyle name of the component
+ * @param string $area the name of the gradable area
+ * @return grading_manager
+ */
+function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
+    global $DB;
+
+    $manager = new grading_manager();
+
+    if (is_object($context_or_areaid)) {
+        $context = $context_or_areaid;
+    } else {
+        $context = null;
+
+        if (is_numeric($context_or_areaid)) {
+            $manager->load($context_or_areaid);
+            return $manager;
+        }
+    }
+
+    if (!is_null($context)) {
+        $manager->set_context($context);
+    }
+
+    if (!is_null($component)) {
+        $manager->set_component($component);
+    }
+
+    if (!is_null($area)) {
+        $manager->set_area($area);
+    }
+
+    return $manager;
+}
+
+/**
+ * General class providing access to common grading features
+ *
+ * Grading manager provides access to the particular grading method controller
+ * in that area.
+ *
+ * Fully initialized instance of the grading manager operates over a single
+ * gradable area. It is possible to work with a partially initialized manager
+ * that knows just context and component without known area, for example.
+ * It is also possible to change context, component and area of an existing
+ * manager. Such pattern is used when copying form definitions, for example.
+ */
+class grading_manager {
+
+    /** @var stdClass the context */
+    protected $context;
+
+    /** @var string the frankenstyle name of the component */
+    protected $component;
+
+    /** @var string the name of the gradable area */
+    protected $area;
+
+    /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
+    private $areacache = null;
+
+    /**
+     * @return stdClass grading manager context
+     */
+    public function get_context() {
+        return $this->context;
+    }
+
+    /**
+     * Sets the context the manager operates on
+     *
+     * @param stdClass $context
+     */
+    public function set_context(stdClass $context) {
+        $this->areacache = null;
+        $this->context = $context;
+    }
+
+    /**
+     * @return string grading manager component
+     */
+    public function get_component() {
+        return $this->component;
+    }
+
+    /**
+     * Sets the component the manager operates on
+     *
+     * @param string $component the frankenstyle name of the component
+     */
+    public function set_component($component) {
+        $this->areacache = null;
+        list($type, $name) = normalize_component($component);
+        $this->component = $type.'_'.$name;
+    }
+
+    /**
+     * @return string grading manager area name
+     */
+    public function get_area() {
+        return $this->area;
+    }
+
+    /**
+     * Sets the area the manager operates on
+     *
+     * @param string $area the name of the gradable area
+     */
+    public function set_area($area) {
+        $this->areacache = null;
+        $this->area = $area;
+    }
+
+    /**
+     * Returns a text describing the context and the component
+     *
+     * At the moment this works for gradable areas in course modules. In the future, this
+     * method should be improved so it works for other contexts (blocks, gradebook items etc)
+     * or subplugins.
+     *
+     * @return string
+     */
+    public function get_component_title() {
+
+        $this->ensure_isset(array('context', 'component'));
+
+        if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
+            if ($this->get_component() == 'core_grading') {
+                $title = ''; // we are in the bank UI
+            } else {
+                throw new coding_exception('Unsupported component at the system context');
+            }
+
+        } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
+            list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
+
+            if (!empty($cm->name)) {
+                $title = $cm->name;
+            } else {
+                debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
+                $title = $this->get_component();
+            }
+
+        } else {
+            throw new coding_exception('Unsupported gradable area context level');
+        }
+
+        return $title;
+    }
+
+    /**
+     * Returns the localized title of the currently set area
+     *
+     * @return string
+     */
+    public function get_area_title() {
+
+        if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
+            return '';
+
+        } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
+            $this->ensure_isset(array('context', 'component', 'area'));
+            $areas = $this->get_available_areas();
+            if (array_key_exists($this->get_area(), $areas)) {
+                return $areas[$this->get_area()];
+            } else {
+                debugging('Unknown area!');
+                return '???';
+            }
+
+        } else {
+            throw new coding_exception('Unsupported context level');
+        }
+    }
+
+    /**
+     * Loads the gradable area info from the database
+     *
+     * @param int $areaid
+     */
+    public function load($areaid) {
+        global $DB;
+
+        $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
+        $this->context = get_context_instance_by_id($this->areacache->contextid, MUST_EXIST);
+        $this->component = $this->areacache->component;
+        $this->area = $this->areacache->areaname;
+    }
+
+    /**
+     * Returns the list of available grading methods in the given context
+     *
+     * Basically this returns the list of installed grading plugins with an empty value
+     * for simple direct grading. In the future, the list of available methods may be
+     * controlled per-context.
+     *
+     * Requires the context property to be set in advance.
+     *
+     * @param bool $includenone should the 'Simple direct grading' be included
+     * @return array of the (string)name => (string)localized title of the method
+     */
+    public function get_available_methods($includenone = true) {
+
+        $this->ensure_isset(array('context'));
+
+        if ($includenone) {
+            $list = array('' => get_string('gradingmethodnone', 'core_grading'));
+        } else {
+            $list = array();
+        }
+
+        foreach (get_plugin_list('gradingform') as $name => $location) {
+            $list[$name] = get_string('pluginname', 'gradingform_'.$name);
+        }
+
+        return $list;
+    }
+
+    /**
+     * Returns the list of gradable areas in the given context and component
+     *
+     * This performs a callback to the library of the relevant plugin to obtain
+     * the list of supported areas.
+     * @return array of (string)areacode => (string)localized title of the area
+     */
+    public function get_available_areas() {
+        global $CFG;
+
+        $this->ensure_isset(array('context', 'component'));
+
+        // example: if the given context+component lead to mod_assignment, this method
+        // will do something like
+        // require_once($CFG->dirroot.'/mod/assignment/lib.php');
+        // return assignment_gradable_area_list();
+
+        // todo - what to return for bank areas in the system context
+        // todo - hardcoded list for now
+        return array('submission' => 'Submissions');
+    }
+
+    /**
+     * Returns the currently active grading method in the gradable area
+     *
+     * @return string|null the name of the grading plugin of null if it has not been set
+     */
+    public function get_active_method() {
+        global $DB;
+
+        $this->ensure_isset(array('context', 'component', 'area'));
+
+        // get the current grading area record if it exists
+        if (is_null($this->areacache)) {
+            $this->areacache = $DB->get_record('grading_areas', array(
+                'contextid' => $this->context->id,
+                'component' => $this->component,
+                'areaname'  => $this->area),
+            '*', IGNORE_MISSING);
+        }
+
+        if ($this->areacache === false) {
+            // no area record yet
+            return null;
+        }
+
+        return $this->areacache->activemethod;
+    }
+
+    /**
+     * Sets the currently active grading method in the gradable area
+     *
+     * @param string $method the method name, eg 'rubric' (must be available)
+     * @return bool true if the method changed or was just set, false otherwise
+     */
+    public function set_active_method($method) {
+        global $DB;
+
+        $this->ensure_isset(array('context', 'component', 'area'));
+
+        // make sure the passed method is empty or a valid plugin name
+        if (empty($method)) {
+            $method = null;
+        } else {
+            if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
+                throw new moodle_exception('invalid_method_name', 'core_grading');
+            }
+            $available = $this->get_available_methods(false);
+            if (!array_key_exists($method, $available)) {
+                throw new moodle_exception('invalid_method_name', 'core_grading');
+            }
+        }
+
+        // get the current grading area record if it exists
+        if (is_null($this->areacache)) {
+            $this->areacache = $DB->get_record('grading_areas', array(
+                'contextid' => $this->context->id,
+                'component' => $this->component,
+                'areaname'  => $this->area),
+            '*', IGNORE_MISSING);
+        }
+
+        $methodchanged = false;
+
+        if ($this->areacache === false) {
+            // no area record yet, create one with the active method set
+            $area = array(
+                'contextid'     => $this->context->id,
+                'component'     => $this->component,
+                'areaname'      => $this->area,
+                'activemethod'  => $method);
+            $DB->insert_record('grading_areas', $area);
+            $methodchanged = true;
+
+        } else {
+            // update the existing record if needed
+            if ($this->areacache->activemethod !== $method) {
+                $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
+                $methodchanged = true;
+            }
+        }
+
+        $this->areacache = null;
+
+        return $methodchanged;
+    }
+
+    /**
+     * Extends the settings navigation with the grading settings
+     *
+     * This function is called when the context for the page is an activity module with the
+     * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms.
+     *
+     * @param settings_navigation $settingsnav {@link settings_navigation}
+     * @param navigation_node $modulenode {@link navigation_node}
+     */
+    public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
+
+        $this->ensure_isset(array('context', 'component'));
+
+        $areas = $this->get_available_areas();
+
+        if (empty($areas)) {
+            // no money, no funny
+            return;
+
+        } else if (count($areas) == 1) {
+            // make just a single node for the management screen
+            $areatitle = reset($areas);
+            $areaname  = key($areas);
+            $this->set_area($areaname);
+            $method = $this->get_active_method();
+            $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
+                $this->get_management_url(), settings_navigation::TYPE_CUSTOM);
+            if ($method) {
+                $controller = $this->get_controller($method);
+                $controller->extend_settings_navigation($settingsnav, $managementnode);
+            }
+
+        } else {
+            // make management screen node for each area
+            $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
+                null, settings_navigation::TYPE_CUSTOM);
+            foreach ($areas as $areaname => $areatitle) {
+                $this->set_area($areaname);
+                $method = $this->get_active_method();
+                $node = $managementnode->add($areatitle,
+                    $this->get_management_url(), settings_navigation::TYPE_CUSTOM);
+                if ($method) {
+                    $controller = $this->get_controller($method);
+                    $controller->extend_settings_navigation($settingsnav, $node);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the given method's controller in the gradable area
+     *
+     * @param string $method the method name, eg 'rubric' (must be available)
+     * @return grading_controller
+     */
+    public function get_controller($method) {
+        global $CFG;
+
+        $this->ensure_isset(array('context', 'component', 'area'));
+
+        // make sure the passed method is a valid plugin name
+        if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
+            throw new moodle_exception('invalid_method_name', 'core_grading');
+        }
+        $available = $this->get_available_methods(false);
+        if (!array_key_exists($method, $available)) {
+            throw new moodle_exception('invalid_method_name', 'core_grading');
+        }
+
+        // get the current grading area record if it exists
+        if (is_null($this->areacache)) {
+            $this->areacache = $DB->get_record('grading_areas', array(
+                'contextid' => $this->context->id,
+                'component' => $this->component,
+                'areaname'  => $this->area),
+            '*', IGNORE_MISSING);
+        }
+
+        if ($this->areacache === false) {
+            // no area record yet, create one
+            $area = array(
+                'contextid' => $this->context->id,
+                'component' => $this->component,
+                'areaname'  => $this->area);
+            $areaid = $DB->insert_record('grading_areas', $area);
+            // reload the cache
+            $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
+        }
+
+        require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
+        $classname = 'gradingform_'.$method.'_controller';
+
+        return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
+    }
+
+    /**
+     * Returns the controller for the active method if it is available
+     *
+     * @return null|grading_controller
+     */
+    public function get_active_controller() {
+        if ($gradingmethod = $this->get_active_method()) {
+            $controller = $this->get_controller($gradingmethod);
+            if ($controller->is_form_available()) {
+                return $controller;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the URL of the grading area management page
+     *
+     * @param moodle_url $returnurl optional URL of the page where the user should be sent back to
+     * @return moodle_url
+     */
+    public function get_management_url(moodle_url $returnurl = null) {
+
+        $this->ensure_isset(array('context', 'component'));
+
+        if ($this->areacache) {
+            $params = array('areaid' => $this->areacache->id);
+        } else {
+            $params = array('contextid' => $this->context->id, 'component' => $this->component);
+            if ($this->area) {
+                $params['area'] = $this->area;
+            }
+        }
+
+        if (!is_null($returnurl)) {
+            $params['returnurl'] = $returnurl->out(false);
+        }
+
+        return new moodle_url('/grade/grading/manage.php', $params);
+    }
+
+    /**
+     * Creates a new shared area to hold a grading form template
+     *
+     * Shared area are implemented as virtual gradable areas at the system level context
+     * with the component set to core_grading and unique random area name.
+     *
+     * @param string $method the name of the plugin we create the area for
+     * @return int the new area id
+     */
+    public function create_shared_area($method) {
+        global $DB;
+
+        // generate some unique random name for the new area
+        $name = $method . '_' . sha1(rand().uniqid($method, true));
+        // create new area record
+        $area = array(
+            'contextid'     => get_system_context()->id,
+            'component'     => 'core_grading',
+            'areaname'      => $name,
+            'activemethod'  => $method);
+        return $DB->insert_record('grading_areas', $area);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Make sure that the given properties were set to some not-null value
+     *
+     * @param array $properties the list of properties
+     * @throws coding_exception
+     */
+    private function ensure_isset(array $properties) {
+        foreach ($properties as $property) {
+            if (!isset($this->$property)) {
+                throw new coding_exception('The property "'.$property.'" is not set.');
+            }
+        }
+    }
+}
diff --git a/grade/grading/manage.php b/grade/grading/manage.php
new file mode 100644 (file)
index 0000000..06ee3d1
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A single gradable area management page
+ *
+ * This page alows the user to set the current active method in the given
+ * area, provides access to the plugin editor and allows user to save the
+ * current form as a template or re-use some existing form.
+ *
+ * @package    core
+ * @subpackage grading
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(dirname(dirname(__FILE__))).'/config.php');
+require_once($CFG->dirroot.'/grade/grading/lib.php');
+
+// identify gradable area by its id
+$areaid     = optional_param('areaid', null, PARAM_INT);
+// alternatively the context, component and areaname must be provided
+$contextid  = optional_param('contextid', null, PARAM_INT);
+$component  = optional_param('component', null, PARAM_COMPONENT);
+$area       = optional_param('area', null, PARAM_AREA);
+// keep the caller's URL so that we know where to send the user finally
+$returnurl  = optional_param('returnurl', null, PARAM_LOCALURL);
+// active method selector
+$setmethod  = optional_param('setmethod', null, PARAM_PLUGIN);
+// publish the given form definition as a new template in the forms bank
+$shareform  = optional_param('shareform', null, PARAM_INT);
+// delete the given form definition
+$deleteform = optional_param('deleteform', null, PARAM_INT);
+// consider the required action as confirmed
+$confirmed  = optional_param('confirmed', false, PARAM_BOOL);
+// a message to display, typically a previous action's result
+$message    = optional_param('message', null, PARAM_NOTAGS);
+
+if (!is_null($areaid)) {
+    // get manager by id
+    $manager = get_grading_manager($areaid);
+} else {
+    // get manager by context and component
+    if (is_null($contextid) or is_null($component) or is_null($area)) {
+        throw new coding_exception('The caller script must identify the gradable area.');
+    }
+    $context = get_context_instance_by_id($contextid, MUST_EXIST);
+    $manager = get_grading_manager($context, $component, $area);
+}
+
+if ($manager->get_context()->contextlevel < CONTEXT_COURSE) {
+    throw new coding_exception('Unsupported gradable area context level');
+}
+
+// get the currently active method
+$method = $manager->get_active_method();
+
+list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
+
+require_login($course, true, $cm);
+require_capability('moodle/grade:managegradingforms', $context);
+
+if (!empty($returnurl)) {
+    $returnurl = new moodle_url($returnurl);
+} else {
+    $returnurl = null;
+}
+
+$PAGE->set_url($manager->get_management_url($returnurl));
+navigation_node::override_active_url($manager->get_management_url());
+$PAGE->set_title(get_string('gradingmanagement', 'core_grading'));
+$PAGE->set_heading(get_string('gradingmanagement', 'core_grading'));
+$output = $PAGE->get_renderer('core_grading');
+
+// process the eventual change of the active grading method
+if (!empty($setmethod)) {
+    require_sesskey();
+    if ($setmethod == 'none') {
+        // here we expect that noone would actually want to call their plugin as 'none'
+        $setmethod = null;
+    }
+    $manager->set_active_method($setmethod);
+    redirect($PAGE->url);
+}
+
+// publish the form as a template
+if (!empty($shareform)) {
+    require_capability('moodle/grade:sharegradingforms', get_system_context());
+    $controller = $manager->get_controller($method);
+    $definition = $controller->get_definition();
+    if (!$confirmed) {
+        // let the user confirm they understand what they are doing (haha ;-)
+        echo $output->header();
+        echo $output->confirm(get_string('manageactionshareconfirm', 'core_grading', s($definition->name)),
+            new moodle_url($PAGE->url, array('shareform' => $shareform, 'confirmed' => 1)),
+            $PAGE->url);
+        echo $output->footer();
+        die();
+    } else {
+        require_sesskey();
+        $newareaid = $manager->create_shared_area($method);
+        $targetarea = get_grading_manager($newareaid);
+        $targetcontroller = $targetarea->get_controller($method);
+        $targetcontroller->update_definition($controller->get_definition_copy($targetcontroller));
+        redirect(new moodle_url($PAGE->url, array('message' => get_string('manageactionsharedone', 'core_grading'))));
+    }
+}
+
+// delete the form definition
+if (!empty($deleteform)) {
+    $controller = $manager->get_controller($method);
+    $definition = $controller->get_definition();
+    if (!$confirmed) {
+        // let the user confirm they understand the consequences (also known as WTF-effect)
+        echo $output->header();
+        echo $output->confirm(markdown_to_html(get_string('manageactiondeleteconfirm', 'core_grading', array(
+            'formname'  => s($definition->name),
+            'component' => $manager->get_component_title(),
+            'area'      => $manager->get_area_title()))),
+            new moodle_url($PAGE->url, array('deleteform' => $deleteform, 'confirmed' => 1)), $PAGE->url);
+        echo $output->footer();
+        die();
+    } else {
+        require_sesskey();
+        $controller->delete_definition();
+        redirect(new moodle_url($PAGE->url, array('message' => get_string('manageactiondeletedone', 'core_grading'))));
+    }
+}
+
+echo $output->header();
+
+if (!empty($message)) {
+    echo $output->management_message($message);
+}
+
+echo $output->heading(get_string('gradingmanagementtitle', 'core_grading', array(
+    'component' => $manager->get_component_title(), 'area' => $manager->get_area_title())));
+
+// display the active grading method information and selector
+echo $output->management_method_selector($manager, $PAGE->url);
+
+// get the currently active method's controller
+if (!empty($method)) {
+    $controller = $manager->get_controller($method);
+    // display relevant actions
+    echo $output->container_start('actions');
+    if ($controller->is_form_defined()) {
+        $definition = $controller->get_definition();
+        echo $output->management_action_icon($controller->get_editor_url($returnurl),
+            get_string('manageactionedit', 'core_grading'), 'b/document-edit');
+        echo $output->management_action_icon(new moodle_url($PAGE->url, array('deleteform' => $definition->id)),
+            get_string('manageactiondelete', 'core_grading'), 'b/edit-delete');
+        if (has_capability('moodle/grade:sharegradingforms', get_system_context())) {
+            echo $output->management_action_icon(new moodle_url($PAGE->url, array('shareform' => $definition->id)),
+                get_string('manageactionshare', 'core_grading'), 'b/bookmark-new');
+        }
+    } else {
+        echo $output->management_action_icon($controller->get_editor_url($returnurl),
+            get_string('manageactionnew', 'core_grading'), 'b/document-new');
+        $pickurl = new moodle_url('/grade/grading/pick.php', array('targetid' => $controller->get_areaid()));
+        if (!is_null($returnurl)) {
+            $pickurl->param('returnurl', $returnurl->out(false));
+        }
+        echo $output->management_action_icon($pickurl,
+            get_string('manageactionclone', 'core_grading'), 'b/edit-copy');
+    }
+    echo $output->container_end();
+
+    // display the grading form preview
+    if ($controller->is_form_defined()) {
+        echo $output->box($controller->render_preview($PAGE), 'preview');
+    }
+}
+
+
+echo $output->footer();
diff --git a/grade/grading/renderer.php b/grade/grading/renderer.php
new file mode 100644 (file)
index 0000000..526f4a5
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer for core_grading subsystem
+ *
+ * @package    core
+ * @subpackage grading
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Standard HTML output renderer for core_grading subsystem
+ */
+class core_grading_renderer extends plugin_renderer_base {
+
+    /**
+     * Renders the active method selector at the grading method management screen
+     *
+     * @param grading_manager $gradingman
+     * @param moodle_url $targeturl
+     * @return string
+     */
+    public function management_method_selector(grading_manager $manager, moodle_url $targeturl) {
+
+        $method = $manager->get_active_method();
+        $methods = $manager->get_available_methods(false);
+        $methods['none'] = get_string('gradingmethodnone', 'core_grading');
+        $selector = new single_select(new moodle_url($targeturl, array('sesskey' => sesskey())),
+            'setmethod', $methods, empty($method) ? 'none' : $method, null, 'activemethodselector');
+        $selector->set_label(get_string('changeactivemethod', 'core_grading'));
+        $selector->set_help_icon('gradingmethod', 'core_grading');
+
+        return $this->output->render($selector);
+    }
+
+    /**
+     * Renders an action icon at the gradng method management screen
+     *
+     * @param moodle_url $url action URL
+     * @param string $text action text
+     * @param string $icon the name of the icon to use
+     * @return string
+     */
+    public function management_action_icon(moodle_url $url, $text, $icon) {
+
+        $img = html_writer::empty_tag('img', array('src' => $this->output->pix_url($icon), 'class' => 'action-icon'));
+        $txt = html_writer::tag('div', $text, array('class' => 'action-text'));
+        return html_writer::link($url, $img . $txt, array('class' => 'action'));
+    }
+
+    /**
+     * Renders a message for the user, typically as an action result
+     *
+     * @param string $message
+     * @return string
+     */
+    public function management_message($message) {
+        $this->page->requires->strings_for_js(array('clicktoclose'), 'core_grading');
+        $this->page->requires->yui_module('moodle-core_grading-manage', 'M.core_grading.init_manage');
+        return $this->output->box(format_string($message).html_writer::tag('span', ''), 'message', 'actionresultmessagebox');
+    }
+
+    /**
+     * Renders the common information about the form definition
+     *
+     * @param gradingform_controller $controller
+     * @return string
+     */
+    public function preview_definition_header(gradingform_controller $controller) {
+
+        $definition = $controller->get_definition();
+        // todo make this nicer, append the information about the time created/modified etc
+        return $this->output->heading(format_text($definition->name));
+    }
+}
diff --git a/grade/grading/simpletest/testlib.php b/grade/grading/simpletest/testlib.php
new file mode 100644 (file)
index 0000000..1bb2a06
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the advanced grading subsystem
+ *
+ * @package    core
+ * @subpackage grading
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $DB, $CFG;
+
+if (empty($CFG->unittestprefix)) {
+    die('You must define $CFG->unittestprefix to run these unit tests.');
+}
+
+if ($CFG->unittestprefix == $CFG->prefix) {
+    die('Eh? Do you play with the fire? Fireman Sam won\'t come dude. The unittestprefix must be different from the standard prefix.');
+}
+
+require_once($CFG->dirroot . '/grade/grading/lib.php'); // Include the code to test
+
+/**
+ * Makes protected method accessible for testing purposes
+ */
+class testable_grading_manager extends grading_manager {
+}
+
+/**
+ * Test cases for the grading manager API
+ */
+class grading_manager_test extends UnitTestCase {
+
+    /** @var moodle_database current real driver instance */
+    protected $realDB;
+
+    public function setUp() {
+        global $DB, $CFG;
+
+        $this->realDB = $DB;
+        $dbclass = get_class($this->realDB);
+        $DB = new $dbclass();
+        $DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
+        $dbman = $DB->get_manager();
+
+        // drop everything we have in the mock DB
+        $dbman->delete_tables_from_xmldb_file($CFG->dirroot . '/lib/db/install.xml');
+        // create all tables we need for this test case
+        $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/lib/db/install.xml', 'grading_areas');
+    }
+
+    public function tearDown() {
+        global $DB, $CFG;
+
+        // clean everything we have in the mock DB
+        //$DB->get_manager()->delete_tables_from_xmldb_file($CFG->dirroot . '/lib/db/install.xml');
+        // switch to the real database
+        $DB = $this->realDB;
+    }
+
+    public function test_basic_instantiation() {
+
+        $manager1 = get_grading_manager();
+
+        $fakecontext = (object)array(
+            'id'            => 42,
+            'contextlevel'  => CONTEXT_MODULE,
+            'instanceid'    => 22,
+            'path'          => '/1/3/15/42',
+            'depth'         => 4);
+
+        $manager2 = get_grading_manager($fakecontext);
+        $manager3 = get_grading_manager($fakecontext, 'assignment_upload');
+        $manager4 = get_grading_manager($fakecontext, 'assignment_upload', 'submission');
+    }
+
+    public function test_set_and_get_grading_area() {
+        global $DB;
+
+        sleep(2); // to make sure the microtime will always return unique values
+        $areaname1 = 'area1-' . (string)microtime(true);
+        $areaname2 = 'area2-' . (string)microtime(true);
+        $fakecontext = (object)array(
+            'id'            => 42,
+            'contextlevel'  => CONTEXT_MODULE,
+            'instanceid'    => 22,
+            'path'          => '/1/3/15/42',
+            'depth'         => 4);
+
+        // non-existing area
+        $gradingman = get_grading_manager($fakecontext, 'mod_foobar', $areaname1);
+        $this->assertNull($gradingman->get_active_method());
+
+        // creates area implicitly and sets active method
+        $this->assertTrue($gradingman->set_active_method('rubric'));
+        $this->assertEqual('rubric', $gradingman->get_active_method());
+
+        // repeat setting of already set active method
+        $this->assertFalse($gradingman->set_active_method('rubric'));
+
+        // switch the manager to another area
+        $gradingman->set_area($areaname2);
+        $this->assertNull($gradingman->get_active_method());
+
+        // switch back and ask again
+        $gradingman->set_area($areaname1);
+        $this->assertEqual('rubric', $gradingman->get_active_method());
+
+        // attempting to set an invalid method
+        $this->expectException('moodle_exception');
+        $gradingman->set_active_method('no_one_should_ever_try_to_implement_a_method_with_this_silly_name');
+    }
+}
diff --git a/grade/grading/yui/manage/manage.js b/grade/grading/yui/manage/manage.js
new file mode 100644 (file)
index 0000000..935c383
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * YUI module for advanced grading methods - the manage page
+ *
+ * @author David Mudrak <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+YUI.add('moodle-core_grading-manage', function(Y) {
+
+    var MANAGE = function() {
+        MANAGE.superclass.constructor.apply(this, arguments);
+    }
+
+    Y.extend(MANAGE, Y.Base, {
+
+        initializer : function(config) {
+            this.setup_messagebox();
+        },
+
+        setup_messagebox : function() {
+            Y.one('#actionresultmessagebox span').setContent(M.util.get_string('clicktoclose', 'core_grading'));
+            Y.one('#actionresultmessagebox').on('click', function(e) {
+                e.halt();
+                var box = e.currentTarget;
+                var anim = new Y.Anim({
+                    node: box,
+                    duration: 1,
+                    to: { opacity: 0, height: 0 },
+                });
+                anim.run();
+                anim.on('end', function() {
+                    var box = this.get('node'); // this === anim
+                    box.remove(true);
+                });
+            });
+        }
+
+    }, {
+        NAME : 'grading_manage_page',
+        ATTRS : { }
+    });
+
+    M.core_grading = M.core_grading || {};
+
+    M.core_grading.init_manage = function(config) {
+        return new MANAGE(config);
+    }
+
+}, '@VERSION@', { requires:['base', 'anim'] });
diff --git a/lang/en/grading.php b/lang/en/grading.php
new file mode 100644 (file)
index 0000000..12257df
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the advanced grading methods subsystem
+ *
+ * @package    core
+ * @subpackage grading
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['activemethodinfo'] = '\'{$a->method}\' is selected as the active grading method for the \'{$a->area}\' area';
+$string['activemethodinfonone'] = 'There is no advanced grading method selected for the \'{$a->area}\' area. Simple direct grading will be used.';
+$string['changeactivemethod'] = 'Change active grading method to';
+$string['clicktoclose'] = 'click to close';
+$string['exc_gradingformelement'] = 'Unable to instantiate grading form element';
+$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Settings block.';
+$string['gradingmanagement'] = 'Advanced grading';
+$string['gradingmanagementtitle'] = 'Advanced grading: {$a->component} ({$a->area})';
+$string['gradingmethod'] = 'Grading method';
+$string['gradingmethod_help'] = 'Choose the advanced grading method that should be used for calculating grades in the given context.
+
+To disable advance grading and switch back to the default grading mechanism, choose \'Simple direct grading\'.';
+$string['gradingmethodnone'] = 'Simple direct grading';
+$string['gradingmethods'] = 'Grading methods';
+$string['manageactionclone'] = 'Create new grading form from a template';
+$string['manageactiondelete'] = 'Remove the currently defined form';
+$string['manageactiondeleteconfirm'] = 'You are going to remove the grading form \'{$a->formname}\' and all the associated information from \'{$a->component} ({$a->area})\'. Please make sure you understand the following consequences:
+
+* There is no way to undo this operation.
+* You can switch to another grading method including the \'Simple direct grading\' without removing this form.
+* All the information about how the grading forms are filled will be lost.
+* The calculated result grades stored in the gradebook will not be affected. However the explanation of how they were calculated will not be available.
+* This operation does not affect eventual copies of this form in other activities.';
+$string['manageactiondeletedone'] = 'The form was successfully deleted';
+$string['manageactionedit'] = 'Edit the current form definition';
+$string['manageactionnew'] = 'Define new grading form from scratch';
+$string['manageactionshare'] = 'Publish the form as a new template';
+$string['manageactionshareconfirm'] = 'You are going to save a copy of the grading form \'{$a}\' as a new public template. Other users at your site will be able to create new grading forms in their activities from that template. Note that users are able to reuse their own grading forms in other activities even if the forms were not saved as template.';
+$string['manageactionsharedone'] = 'The form was successfully saved as a template';
+$string['noitemid'] = 'Grading not possible. The graded item does not exist.';
index 74a3eb2..56498d6 100644 (file)
@@ -76,6 +76,8 @@ $string['type_gradeimport'] = 'Grade import method';
 $string['type_gradeimport_plural'] = 'Grade import methods';
 $string['type_gradereport'] = 'Gradebook report';
 $string['type_gradereport_plural'] = 'Gradebook reports';
+$string['type_gradingform'] = 'Advanced grading method';
+$string['type_gradingform_plural'] = 'Advanced grading methods';
 $string['type_local'] = 'Local plugin';
 $string['type_local_plural'] = 'Local plugins';
 $string['type_message'] = 'Messaging processor';
index bda249f..6d4de5b 100644 (file)
@@ -180,6 +180,8 @@ $string['grade:hide'] = 'Hide/unhide grades or items';
 $string['grade:import'] = 'Import grades';
 $string['grade:lock'] = 'Lock grades or items';
 $string['grade:manage'] = 'Manage grade items';
+$string['grade:managegradingforms'] = 'Manage advanced grading methods';
+$string['grade:sharegradingforms'] = 'Share advanced grading forms';
 $string['grade:manageletters'] = 'Manage letter grades';
 $string['grade:manageoutcomes'] = 'Manage grade outcomes';
 $string['grade:override'] = 'Override grades';
index 667e4b2..296f6b3 100644 (file)
@@ -1487,6 +1487,26 @@ $capabilities = array(
         'clonepermissionsfrom' => 'moodle/course:managegrades'
     ),
 
+    'moodle/grade:managegradingforms' => array(
+        'riskbitmask' => RISK_PERSONAL | RISK_XSS,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        ),
+        'clonepermissionsfrom' => 'moodle/course:managegrades'
+    ),
+
+    'moodle/grade:sharegradingforms' => array(
+        'riskbitmask' => RISK_SPAM,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW
+        ),
+    ),
+
     'moodle/grade:manageoutcomes' => array(
         'captype' => 'write',
         'contextlevel' => CONTEXT_COURSE,
index e05f3a6..967061d 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20111007" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20111014" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <INDEX NAME="timecreated_runreference_ix" UNIQUE="false" FIELDS="timecreated, runreference" PREVIOUS="url_runreference_ix"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="course_published" COMMENT="Information about how and when an local courses were published to hubs" PREVIOUS="profiling">
+    <TABLE NAME="course_published" COMMENT="Information about how and when an local courses were published to hubs" PREVIOUS="profiling" NEXT="grading_areas">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="huburl"/>
         <FIELD NAME="huburl" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="the url of the &quot;registered on&quot; hub" PREVIOUS="id" NEXT="courseid"/>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="grading_areas" COMMENT="Identifies gradable areas where advanced grading can happen. For each area, the current active plugin can be set." PREVIOUS="course_published" NEXT="grading_definitions">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="contextid"/>
+        <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The context of the gradable area, eg module instance context." PREVIOUS="id" NEXT="component"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Frankenstyle name of the component holding this area" PREVIOUS="contextid" NEXT="areaname"/>
+        <FIELD NAME="areaname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="The name of gradable area" PREVIOUS="component" NEXT="activemethod"/>
+        <FIELD NAME="activemethod" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="The default grading method (plugin) that should be used for this area" PREVIOUS="areaname"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="uq_gradable_area"/>
+        <KEY NAME="uq_gradable_area" TYPE="unique" FIELDS="contextid, component, areaname" PREVIOUS="primary" NEXT="fk_context"/>
+        <KEY NAME="fk_context" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id" PREVIOUS="uq_gradable_area"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="grading_definitions" COMMENT="Contains the basic information about an advanced grading form defined in the given gradable area" PREVIOUS="grading_areas" NEXT="grading_instances">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="areaid"/>
+        <FIELD NAME="areaid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="id" NEXT="method"/>
+        <FIELD NAME="method" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="The name of the plugin providing this grading form" PREVIOUS="areaid" NEXT="name"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The title of the form that helps users to identify it" PREVIOUS="method" NEXT="description"/>
+        <FIELD NAME="description" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" COMMENT="More detailed description of the form" PREVIOUS="name" NEXT="descriptionformat"/>
+        <FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="Format of the description field" PREVIOUS="description" NEXT="status"/>
+        <FIELD NAME="status" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" COMMENT="Status of the form definition, by default in the under-construction state" PREVIOUS="descriptionformat" NEXT="copiedfromid"/>
+        <FIELD NAME="copiedfromid" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="The id of the original definition that this was initially copied from or null if it was from scratch" PREVIOUS="status" NEXT="timecreated"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The timestamp of when the form definition was created initially" PREVIOUS="copiedfromid" NEXT="usercreated"/>
+        <FIELD NAME="usercreated" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the user who created this definition and is considered as its owner for access control purposes" PREVIOUS="timecreated" NEXT="timemodified"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The time stamp of when the form definition was modified recently" PREVIOUS="usercreated" NEXT="usermodified"/>
+        <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the user who did the most recent modification" PREVIOUS="timemodified" NEXT="options"/>
+        <FIELD NAME="options" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" COMMENT="General field to be used by plugins as a general storage place for their own settings" PREVIOUS="usermodified"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_areaid"/>
+        <KEY NAME="fk_areaid" TYPE="foreign" FIELDS="areaid" REFTABLE="grading_areas" REFFIELDS="id" PREVIOUS="primary" NEXT="fk_usermodified"/>
+        <KEY NAME="fk_usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id" PREVIOUS="fk_areaid" NEXT="uq_area_method"/>
+        <KEY NAME="uq_area_method" TYPE="unique" FIELDS="areaid, method" PREVIOUS="fk_usermodified" NEXT="fk_usercreated"/>
+        <KEY NAME="fk_usercreated" TYPE="foreign" FIELDS="usercreated" REFTABLE="user" REFFIELDS="id" PREVIOUS="uq_area_method"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="grading_instances" COMMENT="Grading form instance is an assessment record for one gradable item assessed by one rater" PREVIOUS="grading_definitions">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="formid"/>
+        <FIELD NAME="formid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the form definition this is instance of" PREVIOUS="id" NEXT="raterid"/>
+        <FIELD NAME="raterid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The ID of the user who did the assessment" PREVIOUS="formid" NEXT="itemid"/>
+        <FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="This identifies the graded item within the grabable area" PREVIOUS="raterid" NEXT="rawgrade"/>
+        <FIELD NAME="rawgrade" TYPE="number" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" DECIMALS="5" COMMENT="The raw normalized grade 0.00000 - 100.00000 as a result of the most recent assessment" PREVIOUS="itemid" NEXT="status"/>
+        <FIELD NAME="status" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" COMMENT="The status of the assessment. By default the instance is under-assessment state" PREVIOUS="rawgrade" NEXT="feedback"/>
+        <FIELD NAME="feedback" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" COMMENT="Overall feedback from the rater for the author of the graded item" PREVIOUS="status" NEXT="feedbackformat"/>
+        <FIELD NAME="feedbackformat" TYPE="int" LENGTH="2" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="The format of the feedback field" PREVIOUS="feedback" NEXT="timemodified"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" COMMENT="The timestamp of when the assessment was most recently modified" PREVIOUS="feedbackformat"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_formid"/>
+        <KEY NAME="fk_formid" TYPE="foreign" FIELDS="formid" REFTABLE="grading_definitions" REFFIELDS="id" PREVIOUS="primary" NEXT="fk_raterid"/>
+        <KEY NAME="fk_raterid" TYPE="foreign" FIELDS="raterid" REFTABLE="user" REFFIELDS="id" PREVIOUS="fk_formid"/>
+      </KEYS>
+    </TABLE>
   </TABLES>
 </XMLDB>
\ No newline at end of file
index 0184da5..6aa33d1 100644 (file)
@@ -6770,6 +6770,67 @@ FROM
         upgrade_main_savepoint(true, 2011092800.03);
     }
 
+    // TODO squash this before merging into the master - MDL-29798
+    if ($oldversion < 2011100700.01) {
+        // Create new core tables for the advanced grading subsystem
+
+        $table = new xmldb_table('grading_areas');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('areaname', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('activemethod', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('uq_gradable_area', XMLDB_KEY_UNIQUE, array('contextid', 'component', 'areaname'));
+        $table->add_key('fk_context', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        $table = new xmldb_table('grading_definitions');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('areaid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('method', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
+        $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, null);
+        $table->add_field('status', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_field('copiedfromid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('usercreated', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('options', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('fk_areaid', XMLDB_KEY_FOREIGN, array('areaid'), 'grading_areas', array('id'));
+        $table->add_key('fk_usermodified', XMLDB_KEY_FOREIGN, array('usermodified'), 'user', array('id'));
+        $table->add_key('uq_area_method', XMLDB_KEY_UNIQUE, array('areaid', 'method'));
+        $table->add_key('fk_usercreated', XMLDB_KEY_FOREIGN, array('usercreated'), 'user', array('id'));
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        $table = new xmldb_table('grading_instances');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('formid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('raterid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null);
+        $table->add_field('rawgrade', XMLDB_TYPE_NUMBER, '10, 5', XMLDB_UNSIGNED, null, null, null);
+        $table->add_field('status', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
+        $table->add_field('feedback', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
+        $table->add_field('feedbackformat', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, null);
+        $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('fk_formid', XMLDB_KEY_FOREIGN, array('formid'), 'grading_definitions', array('id'));
+        $table->add_key('fk_raterid', XMLDB_KEY_FOREIGN, array('raterid'), 'user', array('id'));
+        $table->add_key('uq_rater_per_item', XMLDB_KEY_UNIQUE, array('formid', 'raterid', 'itemid'));
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        upgrade_main_savepoint(true, 2011100700.01);
+    }
+
     if ($oldversion < 2011100700.02) {
         // Define field idnumber to be added to course_categories
         $table = new xmldb_table('course_categories');
@@ -6802,6 +6863,15 @@ FROM
         upgrade_main_savepoint(true, 2011101200.01);
     }
 
+    // TODO squash this before merging into the master - MDL-29798
+    if ($oldversion < 2011101200.02) {
+        // drop the unique key uq_rater_per_item (unique)
+        $table = new xmldb_table('grading_instances');
+        $key = new xmldb_key('uq_rater_per_item', XMLDB_KEY_UNIQUE, array('formid', 'raterid', 'itemid'));
+        $dbman->drop_key($table, $key);
+        upgrade_main_savepoint(true, 2011101200.02);
+    }
+
     if ($oldversion < 2011101900.02) {
         // remove unused setting
         unset_config('enablehtmlpurifier');
diff --git a/lib/form/grading.php b/lib/form/grading.php
new file mode 100644 (file)
index 0000000..b64ed1f
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Element-container for advanced grading custom input
+ *
+ * @copyright  2011 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+global $CFG;
+require_once("HTML/QuickForm/element.php");
+require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+
+if (class_exists('HTML_QuickForm')) {
+    HTML_QuickForm::registerRule('gradingvalidated', 'callback', '_validate', 'MoodleQuickForm_grading');
+}
+
+/**
+ * HTML class for a grading element. This is a wrapper for advanced grading plugins.
+ * When adding the 'grading' element to the form, developer must pass an object of
+ * class gradingform_instance as $attributes['gradinginstance']. Otherwise an exception will be
+ * thrown.
+ * This object is responsible for implementing functions to render element html and validate it
+ *
+ * @author       Marina Glancy
+ * @access       public
+ */
+class MoodleQuickForm_grading extends HTML_QuickForm_input{
+    /**
+     * html for help button, if empty then no help
+     *
+     * @var string
+     */
+    var $_helpbutton='';
+
+    /**
+     * Stores attributes passed to the element
+     * @var array
+     */
+    private $gradingattributes;
+
+    function MoodleQuickForm_grading($elementName=null, $elementLabel=null, $attributes=null) {
+        parent::HTML_QuickForm_input($elementName, $elementLabel, $attributes);
+        $this->gradingattributes = $attributes;
+    }
+
+    /**
+     * Helper function to retrieve gradingform_instance passed in element attributes
+     *
+     * @return gradingform_instance
+     */
+    function get_gradinginstance() {
+        if (is_array($this->gradingattributes) && array_key_exists('gradinginstance', $this->gradingattributes)) {
+            return $this->gradingattributes['gradinginstance'];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the input field in HTML
+     *
+     * @return    string
+     */
+    function toHtml(){
+        global $PAGE;
+        return $this->get_gradinginstance()->render_grading_element($PAGE, $this);
+    }
+
+    /**
+     * set html for help button
+     *
+     * @access   public
+     * @param array $help array of arguments to make a help button
+     * @param string $function function name to call to get html
+     */
+    function setHelpButton($helpbuttonargs, $function='helpbutton'){
+        debugging('component setHelpButton() is not used any more, please use $mform->setHelpButton() instead');
+    }
+
+    /**
+     * get html for help button
+     *
+     * @access   public
+     * @return  string html for help button
+     */
+    function getHelpButton(){
+        return $this->_helpbutton;
+    }
+
+    /**
+     * @return string
+     */
+    function getElementTemplateType(){
+        return 'default';
+    }
+
+    /**
+     * Adds necessary rules to the element
+     */
+    function onQuickFormEvent($event, $arg, &$caller) {
+        if ($event == 'createElement') {
+            $attributes = $arg[2];
+            if (!is_array($attributes) || !array_key_exists('gradinginstance', $attributes) || !($attributes['gradinginstance'] instanceof gradingform_instance)) {
+                throw new moodle_exception('exc_gradingformelement', 'grading');
+            }
+        }
+
+        $name = $this->getName();
+        if ($name && $caller->elementExists($name)) {
+            $caller->addRule($name, $this->get_gradinginstance()->default_validation_error_message(), 'gradingvalidated', $this->gradingattributes);
+        }
+        return parent::onQuickFormEvent($event, $arg, $caller);
+    }
+
+    /**
+     * Function registered as rule for this element and is called when this element is being validated.
+     * This is a wrapper to pass the validation to the method gradingform_instance::validate_grading_element
+     */
+    static function _validate($elementValue, $attributes = null) {
+        return $attributes['gradinginstance']->validate_grading_element($elementValue);
+    }
+}
index 45e01a2..f02469e 100644 (file)
@@ -2487,6 +2487,7 @@ MoodleQuickForm::registerElementType('file', "$CFG->libdir/form/file.php", 'Mood
 MoodleQuickForm::registerElementType('filemanager', "$CFG->libdir/form/filemanager.php", 'MoodleQuickForm_filemanager');
 MoodleQuickForm::registerElementType('filepicker', "$CFG->libdir/form/filepicker.php", 'MoodleQuickForm_filepicker');
 MoodleQuickForm::registerElementType('format', "$CFG->libdir/form/format.php", 'MoodleQuickForm_format');
+MoodleQuickForm::registerElementType('grading', "$CFG->libdir/form/grading.php", 'MoodleQuickForm_grading');
 MoodleQuickForm::registerElementType('group', "$CFG->libdir/form/group.php", 'MoodleQuickForm_group');
 MoodleQuickForm::registerElementType('header', "$CFG->libdir/form/header.php", 'MoodleQuickForm_header');
 MoodleQuickForm::registerElementType('hidden', "$CFG->libdir/form/hidden.php", 'MoodleQuickForm_hidden');
index 2a9fed0..b06a479 100644 (file)
@@ -376,6 +376,8 @@ define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
 /** True if module supports outcomes */
 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
+/** True if module supports advanced grading methods */
+define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
 
 /** True if module has code to track whether somebody viewed it */
 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
@@ -7284,6 +7286,7 @@ function get_core_subsystems() {
             'fonts'       => NULL,
             'form'        => 'lib/form',
             'grades'      => 'grade',
+            'grading'     => 'grade/grading',
             'group'       => 'group',
             'help'        => NULL,
             'hub'         => NULL,
@@ -7352,6 +7355,7 @@ function get_plugin_types($fullpaths=true) {
                       'gradeexport'   => 'grade/export',
                       'gradeimport'   => 'grade/import',
                       'gradereport'   => 'grade/report',
+                      'gradingform'   => 'grade/grading/form',
                       'mnetservice'   => 'mnet/service',
                       'webservice'    => 'webservice',
                       'repository'    => 'repository',
index e5463fd..c6333ec 100644 (file)
@@ -3467,6 +3467,14 @@ class settings_navigation extends navigation_node {
             $modulenode->add(get_string('restore'), $url, self::TYPE_SETTING, null, 'restore');
         }
 
+        // Allow the active advanced grading method plugin to append its settings
+        $featuresfunc = $this->page->activityname.'_supports';
+        if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING) && has_capability('moodle/grade:managegradingforms', $this->page->cm->context)) {
+            require_once($CFG->dirroot.'/grade/grading/lib.php');
+            $gradingman = get_grading_manager($this->page->cm->context, $this->page->activityname);
+            $gradingman->extend_settings_navigation($this, $modulenode);
+        }
+
         $function = $this->page->activityname.'_extend_settings_navigation';
         if (!function_exists($function)) {
             return $modulenode;
index 02719a7..6642408 100644 (file)
@@ -348,6 +348,10 @@ class plugin_manager {
                 'grader', 'outcomes', 'overview', 'user'
             ),
 
+            'gradingform' => array(
+                'rubric'
+            ),
+
             'local' => array(
             ),
 
index c4a390c..ec602d8 100644 (file)
@@ -264,8 +264,9 @@ class assignment_base {
      * @param object $submission The submission object or NULL in which case it will be loaded
      */
     function view_feedback($submission=NULL) {
-        global $USER, $CFG, $DB, $OUTPUT;
+        global $USER, $CFG, $DB, $OUTPUT, $PAGE;
         require_once($CFG->libdir.'/gradelib.php');
+        require_once("$CFG->dirroot/grade/grading/lib.php");
 
         if (!is_enrolled($this->context, $USER, 'mod/assignment:view')) {
             // can not submit assignments -> no feedback
@@ -329,9 +330,13 @@ class assignment_base {
         echo '<tr>';
         echo '<td class="left side">&nbsp;</td>';
         echo '<td class="content">';
-        echo '<div class="grade">';
-        echo get_string("grade").': '.$grade->str_long_grade;
-        echo '</div>';
+        $grade_str = '<div class="grade">'. get_string("grade").': '.$grade->str_long_grade. '</div>';
+        if (!empty($submission) && $controller = get_grading_manager($this->context, 'mod_assignment', 'submission')->get_active_controller()) {
+            $controller->set_grade_range(make_grades_menu($this->assignment->grade));
+            echo $controller->render_grade($PAGE, $submission->id, $item, $grade_str);
+        } else {
+            echo $grade_str;
+        }
         echo '<div class="clearer"></div>';
 
         echo '<div class="comment">';
@@ -608,7 +613,6 @@ class assignment_base {
 
         //make user global so we can use the id
         global $USER, $OUTPUT, $DB, $PAGE;
-
         $mailinfo = optional_param('mailinfo', null, PARAM_BOOL);
 
         if (optional_param('next', null, PARAM_BOOL)) {
@@ -628,6 +632,12 @@ class assignment_base {
             set_user_preference('assignment_mailinfo', $mailinfo);
         }
 
+        if (!($this->validate_and_preprocess_feedback())) {
+            // form was submitted ('Save' or 'Save and next' was pressed, but validation failed)
+            $this->display_submission();
+            return;
+        }
+
         switch ($mode) {
             case 'grade':                         // We are in a main window grading
                 if ($submission = $this->process_feedback()) {
@@ -771,6 +781,23 @@ class assignment_base {
         }
     }
 
+    /**
+     * Checks if grading method allows quickgrade mode. At the moment it is hardcoded
+     * that advanced grading methods do not allow quickgrade.
+     *
+     * Assignment type plugins are not allowed to override this method
+     *
+     * @return boolean
+     */
+    public final function quickgrade_mode_allowed() {
+        global $CFG;
+        require_once("$CFG->dirroot/grade/grading/lib.php");
+        if ($controller = get_grading_manager($this->context, 'mod_assignment', 'submission')->get_active_controller()) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Helper method updating the listing on the main script from popup using javascript
      *
@@ -785,7 +812,7 @@ class assignment_base {
 
         $perpage = get_user_preferences('assignment_perpage', 10);
 
-        $quickgrade = get_user_preferences('assignment_quickgrade', 0);
+        $quickgrade = get_user_preferences('assignment_quickgrade', 0) && $this->quickgrade_mode_allowed();
 
         /// Run some Javascript to try and update the parent page
         $output .= '<script type="text/javascript">'."\n<!--\n";
@@ -918,10 +945,11 @@ class assignment_base {
      * @param string $extra_javascript
      */
     function display_submission($offset=-1,$userid =-1, $display=true) {
-        global $CFG, $DB, $PAGE, $OUTPUT;
+        global $CFG, $DB, $PAGE, $OUTPUT, $USER;
         require_once($CFG->libdir.'/gradelib.php');
         require_once($CFG->libdir.'/tablelib.php');
         require_once("$CFG->dirroot/repository/lib.php");
+        require_once("$CFG->dirroot/grade/grading/lib.php");
         if ($userid==-1) {
             $userid = required_param('userid', PARAM_INT);
         }
@@ -1038,13 +1066,22 @@ class assignment_base {
         } elseif ($assignment->assignmenttype == 'uploadsingle') {
             $mformdata->fileui_options = array('subdirs'=>0, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL);
         }
+        if ($controller = get_grading_manager($this->context, 'mod_assignment', 'submission')->get_active_controller()) {
+            $itemid = null;
+            if (!empty($submission->id)) {
+                $itemid = $submission->id;
+            }
+            $mformdata->advancedgradinginstance = $controller->create_instance($USER->id, $itemid);
+        }
 
         $submitform = new mod_assignment_grading_form( null, $mformdata );
 
          if (!$display) {
             $ret_data = new stdClass();
             $ret_data->mform = $submitform;
-            $ret_data->fileui_options = $mformdata->fileui_options;
+            if (isset($mformdata->fileui_options)) {
+                $ret_data->fileui_options = $mformdata->fileui_options;
+            }
             return $ret_data;
         }
 
@@ -1119,7 +1156,7 @@ class assignment_base {
          * from database
          */
         $perpage    = get_user_preferences('assignment_perpage', 10);
-        $quickgrade = get_user_preferences('assignment_quickgrade', 0);
+        $quickgrade = get_user_preferences('assignment_quickgrade', 0) && $this->quickgrade_mode_allowed();
         $filter = get_user_preferences('assignment_filter', 0);
         $grading_info = grade_get_grades($this->course->id, 'mod', 'assignment', $this->assignment->id);
 
@@ -1348,6 +1385,7 @@ class assignment_base {
 
                     /// Calculate user status
                         $auser->status = ($auser->timemarked > 0) && ($auser->timemarked >= $auser->timemodified);
+                        // TODO add here code if advanced grading grade must be reviewed => $auser->status=0
                         $picture = $OUTPUT->user_picture($auser);
 
                         if (empty($auser->submissionid)) {
@@ -1533,9 +1571,11 @@ class assignment_base {
         $mform->addElement('text', 'perpage', get_string('pagesize', 'assignment'), array('size'=>1));
         $mform->setDefault('perpage', $perpage);
 
-        $mform->addElement('checkbox', 'quickgrade', get_string('quickgrade','assignment'));
-        $mform->setDefault('quickgrade', $quickgrade);
-        $mform->addHelpButton('quickgrade', 'quickgrade', 'assignment');
+        if ($this->quickgrade_mode_allowed()) {
+            $mform->addElement('checkbox', 'quickgrade', get_string('quickgrade','assignment'));
+            $mform->setDefault('quickgrade', $quickgrade);
+            $mform->addHelpButton('quickgrade', 'quickgrade', 'assignment');
+        }
 
         $mform->addElement('submit', 'savepreferences', get_string('savepreferences'));
 
@@ -1544,6 +1584,43 @@ class assignment_base {
         echo $OUTPUT->footer();
     }
 
+    /**
+     * If the form was cancelled ('Cancel' or 'Next' was pressed), call cancel method
+     * from advanced grading (if applicable) and returns true
+     * If the form was submitted, validates it and returns false if validation did not pass.
+     * If validation passes, preprocess advanced grading (if applicable) and returns true.
+     */
+    function validate_and_preprocess_feedback() {
+        global $USER;
+        if (!($feedback = data_submitted()) || !isset($feedback->userid) || !isset($feedback->offset)) {
+            return true;      // No incoming data, nothing to validate
+        }
+        $userid = required_param('userid', PARAM_INT);
+        $offset = required_param('offset', PARAM_INT);
+        $submissiondata = $this->display_submission($offset, $userid, false);
+        $mform = $submissiondata->mform;
+        $gradinginstance = $mform->use_advanced_grading();
+        if (optional_param('cancel', false, PARAM_BOOL) || optional_param('next', false, PARAM_BOOL)) {
+            // form was cancelled
+            if ($gradinginstance) {
+                $gradinginstance->cancel();
+            }
+        } else if ($mform->is_submitted()) {
+            // form was submitted (= a submit button other than 'cancel' or 'next' has been clicked)
+            if (!$mform->is_validated()) {
+                return false;
+            }
+            // preprocess advanced grading here
+            if ($gradinginstance) {
+                $data = $mform->get_data();
+                // create submission if it did not exist yet because we need submission->id for storing the grading instance
+                $submission = $this->get_submission($userid, true);
+                $_POST['xgrade'] = $gradinginstance->submit_and_get_grade($data->advancedgrading, $submission->id);
+            }
+        }
+        return true;
+    }
+
     /**
      *  Process teacher feedback submission
      *
@@ -2226,6 +2303,10 @@ class mod_assignment_grading_form extends moodleform {
         global $OUTPUT;
         $mform =& $this->_form;
 
+        if (isset($this->_customdata->advancedgradinginstance)) {
+            $this->use_advanced_grading($this->_customdata->advancedgradinginstance);
+        }
+
         $formattr = $mform->getAttributes();
         $formattr['id'] = 'submitform';
         $mform->setAttributes($formattr);
@@ -2271,6 +2352,19 @@ class mod_assignment_grading_form extends moodleform {
 
     }
 
+    private $advancegradinginstance;
+    /**
+     * Gets or sets the instance for advanced grading
+     *
+     * @param gradingform_instance $gradinginstance
+     */
+    public function use_advanced_grading($gradinginstance = false) {
+        if ($gradinginstance !== false) {
+            $this->advancegradinginstance = $gradinginstance;
+        }
+        return $this->advancegradinginstance;
+    }
+
     function add_grades_section() {
         global $CFG;
         $mform =& $this->_form;
@@ -2279,13 +2373,20 @@ class mod_assignment_grading_form extends moodleform {
             $attributes['disabled'] ='disabled';
         }
 
+        $mform->addElement('header', 'Grades', get_string('grades', 'grades'));
+
         $grademenu = make_grades_menu($this->_customdata->assignment->grade);
-        $grademenu['-1'] = get_string('nograde');
+        if ($gradinginstance = $this->use_advanced_grading()) {
+            $gradinginstance->get_controller()->set_grade_range($grademenu);
+            $mform->addElement('grading', 'advancedgrading', get_string('grade').':', array('gradinginstance' => $gradinginstance));
+        } else {
+            // use simple direct grading
+            $grademenu['-1'] = get_string('nograde');
 
-        $mform->addElement('header', 'Grades', get_string('grades', 'grades'));
-        $mform->addElement('select', 'xgrade', get_string('grade').':', $grademenu, $attributes);
-        $mform->setDefault('xgrade', $this->_customdata->submission->grade ); //@fixme some bug when element called 'grade' makes it break
-        $mform->setType('xgrade', PARAM_INT);
+            $mform->addElement('select', 'xgrade', get_string('grade').':', $grademenu, $attributes);
+            $mform->setDefault('xgrade', $this->_customdata->submission->grade ); //@fixme some bug when element called 'grade' makes it break
+            $mform->setType('xgrade', PARAM_INT);
+        }
 
         if (!empty($this->_customdata->enableoutcomes)) {
             foreach($this->_customdata->grading_info->outcomes as $n=>$outcome) {
@@ -2435,6 +2536,11 @@ class mod_assignment_grading_form extends moodleform {
             }
             $data = file_postupdate_standard_editor($data, 'submissioncomment', $editoroptions, $this->_customdata->context, $editoroptions['component'], $editoroptions['filearea'], $itemid);
         }
+
+        if ($this->use_advanced_grading() && !isset($data->advancedgrading)) {
+            $data->advancedgrading = null;
+        }
+
         return $data;
     }
 }
@@ -3681,6 +3787,7 @@ function assignment_supports($feature) {
         case FEATURE_GRADE_HAS_GRADE:         return true;
         case FEATURE_BACKUP_MOODLE2:          return true;
         case FEATURE_SHOW_DESCRIPTION:        return true;
+        case FEATURE_ADVANCED_GRADING:        return true;
 
         default: return null;
     }
diff --git a/pix/b/BIG-ICONS b/pix/b/BIG-ICONS
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pix/b/bookmark-new.png b/pix/b/bookmark-new.png
new file mode 100644 (file)
index 0000000..2d494b4
Binary files /dev/null and b/pix/b/bookmark-new.png differ
diff --git a/pix/b/document-edit.png b/pix/b/document-edit.png
new file mode 100644 (file)
index 0000000..7b53f8f
Binary files /dev/null and b/pix/b/document-edit.png differ
diff --git a/pix/b/document-new.png b/pix/b/document-new.png
new file mode 100644 (file)
index 0000000..0016409
Binary files /dev/null and b/pix/b/document-new.png differ
diff --git a/pix/b/document-properties.png b/pix/b/document-properties.png
new file mode 100644 (file)
index 0000000..3baa39d
Binary files /dev/null and b/pix/b/document-properties.png differ
diff --git a/pix/b/edit-copy.png b/pix/b/edit-copy.png
new file mode 100644 (file)
index 0000000..43c0d2f
Binary files /dev/null and b/pix/b/edit-copy.png differ
diff --git a/pix/b/edit-delete.png b/pix/b/edit-delete.png
new file mode 100644 (file)
index 0000000..0018cec
Binary files /dev/null and b/pix/b/edit-delete.png differ
index 5b901c0..4fb48d5 100644 (file)
@@ -33,4 +33,15 @@ td.grade div.overridden {background-color: #DDDDDD;}
 .gradetreebox tr .cell.level2 {background-color: #D0DBD3; width: 10px;}
 .gradetreebox tr .cell.level3 {background-color: #D0F3D6; width: 10px;}
 .gradetreebox tr .cell.level4 {background-color: #F0F0AA; width: 10px;}
-.gradetreebox tr .cell.level5 {background-color: #EBDEF6; width: 10px;}
\ No newline at end of file
+.gradetreebox tr .cell.level5 {background-color: #EBDEF6; width: 10px;}
+
+/** Advanced grading **/
+#page-grade-grading-manage #activemethodselector {text-align:center;margin-bottom:1em;}
+#page-grade-grading-manage #activemethodselector select {margin:0px 1em;}
+#page-grade-grading-manage .actions {text-align:center;}
+#page-grade-grading-manage .action {display:inline-block;width: 150px;background-color:#EEE;border:2px solid #CCC;
+    margin:0.5em;padding:0.5em;text-align:center;-moz-border-radius:5px}
+#page-grade-grading-manage .action:hover {text-decoration:none;background-color:#F6F6F6;}
+#page-grade-grading-manage #actionresultmessagebox {background-color:#D2EBFF;width:60%;margin:1em auto 1em auto;text-align:center;
+    padding:0.5em;border:2px solid #CCC;text-align:center;-moz-border-radius:5px;position:relative}
+#page-grade-grading-manage #actionresultmessagebox span {position:absolute;right:0px;top:-1.2em;color:#666;font-size:80%}