Merged MDL-29108 Advanced grading

This commit is contained in:
Sam Hemelryk 2011-11-14 22:01:05 +13:00
commit 1ae9aa2b8f
66 changed files with 6231 additions and 51 deletions

View File

@ -148,6 +148,9 @@ abstract class backup_activity_task extends backup_task {
// Generate the grades file
$this->add_step(new backup_activity_grades_structure_step('activity_grades', 'grades.xml'));
// Generate the grading file (conditionally)
$this->add_step(new backup_activity_grading_structure_step('activity_grading', 'grading.xml'));
// Annotate the scales used in already annotated outcomes
$this->add_step(new backup_annotate_scales_from_outcomes('annotate_scales'));

View File

@ -0,0 +1,32 @@
<?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 core
* @subpackage backup-moodle2
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Base class for all advanced grading form plugins
*/
abstract class backup_gradingform_plugin extends backup_plugin {
}

View File

@ -34,6 +34,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_default_block_task.class.ph
require_once($CFG->dirroot . '/backup/moodle2/backup_xml_transformer.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_report_plugin.class.php');

View File

@ -1784,6 +1784,77 @@ class backup_annotate_all_user_files extends backup_execution_step {
}
}
/**
* Defines the backup step for advanced grading methods attached to the activity module
*/
class backup_activity_grading_structure_step extends backup_structure_step {
/**
* Include the grading.xml only if the module supports advanced grading
*/
protected function execute_condition() {
return plugin_supports('mod', $this->get_task()->get_modulename(), FEATURE_ADVANCED_GRADING, false);
}
/**
* Declares the gradable areas structures and data sources
*/
protected function define_structure() {
// To know if we are including userinfo
$userinfo = $this->get_setting_value('userinfo');
// Define the elements
$areas = new backup_nested_element('areas');
$area = new backup_nested_element('area', array('id'), array(
'areaname', 'activemethod'));
$definitions = new backup_nested_element('definitions');
$definition = new backup_nested_element('definition', array('id'), array(
'method', 'name', 'description', 'descriptionformat', 'status',
'timecreated', 'timemodified', 'options'));
$instances = new backup_nested_element('instances');
$instance = new backup_nested_element('instance', array('id'), array(
'raterid', 'itemid', 'rawgrade', 'status', 'feedback',
'feedbackformat', 'timemodified'));
// Build the tree including the method specific structures
// (beware - the order of how gradingform plugins structures are attached is important)
$areas->add_child($area);
$area->add_child($definitions);
$definitions->add_child($definition);
$this->add_plugin_structure('gradingform', $definition, true);
$definition->add_child($instances);
$instances->add_child($instance);
$this->add_plugin_structure('gradingform', $instance, false);
// Define data sources
$area->set_source_table('grading_areas', array('contextid' => backup::VAR_CONTEXTID,
'component' => array('sqlparam' => 'mod_'.$this->get_task()->get_modulename())));
$definition->set_source_table('grading_definitions', array('areaid' => backup::VAR_PARENTID));
if ($userinfo) {
$instance->set_source_table('grading_instances', array('formid' => backup::VAR_PARENTID));
}
// Annotate references
$definition->annotate_files('grading', 'description', 'id');
$instance->annotate_ids('user', 'raterid');
// Return the root element
return $areas;
}
}
/**
* structure step in charge of constructing the grades.xml file for all the grade items
* and letters related to one activity

View File

@ -150,6 +150,9 @@ abstract class restore_activity_task extends restore_task {
// Grades (module-related, rest of gradebook is restored later if possible: cats, calculations...)
$this->add_step(new restore_activity_grades_structure_step('activity_grades', 'grades.xml'));
// Advanced grading methods attached to the module
$this->add_step(new restore_activity_grading_structure_step('activity_grading', 'grading.xml'));
// Userscompletion (conditionally)
if ($this->get_setting_value('userscompletion')) {
$this->add_step(new restore_userscompletion_structure_step('activity_userscompletion', 'completion.xml'));

View File

@ -0,0 +1,42 @@
<?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 core
* @subpackage backup-moodle2
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Base class for all advanced grading form plugins
*/
abstract class restore_gradingform_plugin extends restore_plugin {
/**
* Helper method returning the mapping identifierto use for
* grading form instance's itemid field
*
* @param array $areaname the name of the area the form is defined for
* @return string the mapping identifier
*/
public static function itemid_mapping($areaname) {
return 'grading_item_'.$areaname;
}
}

View File

@ -38,6 +38,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_theme_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_report_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_coursereport_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_plagiarism_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_gradingform_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
@ -45,6 +46,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_report_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_coursereport_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plagiarism_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_subplugin.class.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_settingslib.php');
require_once($CFG->dirroot . '/backup/moodle2/restore_stepslib.php');

View File

@ -1871,6 +1871,119 @@ class restore_activity_logs_structure_step extends restore_course_logs_structure
}
}
/**
* Defines the restore step for advanced grading methods attached to the activity module
*/
class restore_activity_grading_structure_step extends restore_structure_step {
/**
* Declares paths in the grading.xml file we are interested in
*/
protected function define_structure() {
$paths = array();
$userinfo = $this->get_setting_value('userinfo');
$paths[] = new restore_path_element('grading_area', '/areas/area');
$definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
$paths[] = $definition;
$this->add_plugin_structure('gradingform', $definition);
if ($userinfo) {
$instance = new restore_path_element('grading_instance',
'/areas/area/definitions/definition/instances/instance');
$paths[] = $instance;
$this->add_plugin_structure('gradingform', $instance);
}
return $paths;
}
/**
* Processes one grading area element
*
* @param array $data element data
*/
protected function process_grading_area($data) {
global $DB;
$task = $this->get_task();
$data = (object)$data;
$oldid = $data->id;
$data->component = 'mod_'.$task->get_modulename();
$data->contextid = $task->get_contextid();
$newid = $DB->insert_record('grading_areas', $data);
$this->set_mapping('grading_area', $oldid, $newid);
}
/**
* Processes one grading definition element
*
* @param array $data element data
*/
protected function process_grading_definition($data) {
global $DB;
$task = $this->get_task();
$data = (object)$data;
$oldid = $data->id;
$data->areaid = $this->get_new_parentid('grading_area');
$data->copiedfromid = null;
$data->timecreated = time();
$data->usercreated = $task->get_userid();
$data->timemodified = $data->timecreated;
$data->usermodified = $data->usercreated;
$newid = $DB->insert_record('grading_definitions', $data);
$this->set_mapping('grading_definition', $oldid, $newid, true);
}
/**
* Processes one grading form instance element
*
* @param array $data element data
*/
protected function process_grading_instance($data) {
global $DB;
$data = (object)$data;
// new form definition id
$newformid = $this->get_new_parentid('grading_definition');
// get the name of the area we are restoring to
$sql = "SELECT ga.areaname
FROM {grading_definitions} gd
JOIN {grading_areas} ga ON gd.areaid = ga.id
WHERE gd.id = ?";
$areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
// get the mapped itemid - the activity module is expected to define the mappings
// for each gradable area
$newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
$oldid = $data->id;
$data->formid = $newformid;
$data->raterid = $this->get_mappingid('user', $data->raterid);
$data->itemid = $newitemid;
$newid = $DB->insert_record('grading_instances', $data);
$this->set_mapping('grading_instance', $oldid, $newid);
}
/**
* Final operations when the database records are inserted
*/
protected function after_execute() {
// Add files embedded into the definition description
$this->add_related_files('grading', 'description', 'grading_definition');
}
}
/**
* This structure step restores the grade items associated with one activity
* All the grade items are made child of the "course" grade item but the original

View File

@ -86,6 +86,23 @@ if (!empty($add)) {
$data->introeditor = array('text'=>'', 'format'=>FORMAT_HTML, 'itemid'=>$draftid_editor); // TODO: add better default
}
if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false)
and has_capability('moodle/grade:managegradingforms', $context)) {
require_once($CFG->dirroot.'/grade/grading/lib.php');
$data->_advancedgradingdata['methods'] = grading_manager::available_methods();
$areas = grading_manager::available_areas('mod_'.$module->name);
foreach ($areas as $areaname => $areatitle) {
$data->_advancedgradingdata['areas'][$areaname] = array(
'title' => $areatitle,
'method' => '',
);
$formfield = 'advancedgradingmethod_'.$areaname;
$data->{$formfield} = '';
}
}
if (!empty($type)) { //TODO: hopefully will be removed in 2.0
$data->type = $type;
}
@ -148,6 +165,25 @@ if (!empty($add)) {
$data->introeditor = array('text'=>$currentintro, 'format'=>$data->introformat, 'itemid'=>$draftid_editor);
}
if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false)
and has_capability('moodle/grade:managegradingforms', $context)) {
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 +596,37 @@ if ($mform->is_cancelled()) {
}
}
if (plugin_supports('mod', $fromform->modulename, FEATURE_ADVANCED_GRADING, false)
and has_capability('moodle/grade:managegradingforms', $modcontext)) {
require_once($CFG->dirroot.'/grade/grading/lib.php');
$gradingman = get_grading_manager($modcontext, '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}");
}

View 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'),

869
grade/grading/form/lib.php Normal file
View File

@ -0,0 +1,869 @@
<?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 {
/** undefined definition status */
const DEFINITION_STATUS_NULL = 0;
/** the form is currently being edited and is not ready for usage yet */
const DEFINITION_STATUS_DRAFT = 10;
/** the for was marked as ready for actual usage */
const DEFINITION_STATUS_READY = 20;
/** @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;
/** @var boolean|null cached result of function has_active_instances() */
protected $hasactiveinstances = 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 ready for usage?
*
* @return boolean
*/
public function is_form_available() {
return ($this->is_form_defined() && $this->definition->status == self::DEFINITION_STATUS_READY);
}
/**
* Is the grading form saved as a shared public template?
*
* @return boolean
*/
public function is_shared_template() {
return ($this->get_context()->id == context_system::instance()->id
and $this->get_component() == 'core_grading');
}
/**
* Is the grading form owned by the given user?
*
* The form owner is the user who created this instance of the form.
*
* @param int $userid the user id to check, defaults to the current user
* @return boolean|null null if the form not defined yet, boolean otherwise
*/
public function is_own_form($userid = null) {
global $USER;
if (!$this->is_form_defined()) {
return null;
}
if (is_null($userid)) {
$userid = $USER->id;
}
return ($this->definition->usercreated == $userid);
}
/**
* Returns a message why this form is unavailable. Maybe overriden by plugins to give more details.
* @see is_form_available()
*
* @return string
*/
public function form_unavailable_notification() {
if ($this->is_form_available()) {
return null;
}
return get_string('gradingformunavailable', 'grading');
}
/**
* 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;
$new->status = $old->status;
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;
if (empty($record->status)) {
$record->status = self::DEFINITION_STATUS_DRAFT;
}
if (empty($record->descriptionformat)) {
$record->descriptionformat = FORMAT_MOODLE; // field can not be empty
}
$DB->insert_record('grading_definitions', $record);
} else {
throw new coding_exception('Unknown status of the cached definition record.');
}
}
/**
* Formats the definition description for display on page
*
* @return string
*/
public function get_formatted_description() {
if (!isset($this->definition->description)) {
return '';
}
return format_text($this->definition->description, $this->definition->descriptionformat);
}
/**
* Returns the current instance (either with status ACTIVE or NEEDUPDATE) 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;
$params = array(
'definitionid' => $this->definition->id,
'itemid' => $itemid,
'status1' => gradingform_instance::INSTANCE_STATUS_ACTIVE,
'status2' => gradingform_instance::INSTANCE_STATUS_NEEDUPDATE);
$select = 'definitionid=:definitionid and itemid=:itemid and (status=:status1 or status=:status2)';
if (false /* TODO $manager->allow_multiple_raters() */) {
$select .= ' and raterid=:raterid';
$params['raterid'] = $raterid;
}
if ($idonly) {
if ($current = $DB->get_record_select('grading_instances', $select, $params, 'id', IGNORE_MISSING)) {
return $current->id;
}
} else {
if ($current = $DB->get_record_select('grading_instances', $select, $params, '*', IGNORE_MISSING)) {
return $this->get_instance($current);
}
}
return null;
}
/**
* Returns list of ACTIVE instances for the specified $itemid
* (intentionally does not return instances with status NEEDUPDATE)
*
* @param int $itemid
* @return array of gradingform_instance objects
*/
public function get_active_instances($itemid) {
global $DB;
$conditions = array('definitionid' => $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 true if there are already people who has been graded on this definition.
* In this case plugins may restrict changes of the grading definition
*
* @return boolean
*/
public function has_active_instances() {
global $DB;
if (empty($this->definition->id)) {
return false;
}
if ($this->hasactiveinstances === null) {
$conditions = array('definitionid' => $this->definition->id,
'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE);
$this->hasactiveinstances = $DB->record_exists('grading_instances', $conditions);
}
return $this->hasactiveinstances;
}
/**
* 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) {
// 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));
}
}
/**
* If instanceid is specified and grading instance exists and it is created by this rater for
* this item, this instance is returned.
* Otherwise new instance is created for the specified rater and itemid
*
* @param int $instanceid
* @param int $raterid
* @param int $itemid
* @return gradingform_instance
*/
public function get_or_create_instance($instanceid, $raterid, $itemid) {
global $DB;
if ($instanceid &&
$instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
return $this->get_instance($instance);
}
return $this->create_instance($raterid, $itemid);
}
/**
* Returns the HTML code displaying the preview of the grading form
*
* Plugins are forced to override this. Ideally they should delegate
* the task to their own renderer.
*
* @param moodle_page $page the target page
* @return string
*/
abstract public function render_preview(moodle_page $page);
/**
* 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('definitionid' => $this->definition->id));
// finally, delete the main definition record
$DB->delete_records('grading_definitions', array('id' => $this->definition->id));
$this->definition = false;
}
/**
* Prepare the part of the search query to append to the FROM statement
*
* @param string $gdid the alias of grading_definitions.id column used by the caller
* @return string
*/
public static function sql_search_from_tables($gdid) {
return '';
}
/**
* Prepare the parts of the SQL WHERE statement to search for the given token
*
* The returned array cosists of the list of SQL comparions and the list of
* respective parameters for the comparisons. The returned chunks will be joined
* with other conditions using the OR operator.
*
* @param string $token token to search for
* @return array
*/
public static function sql_search_where($token) {
$subsql = array();
$params = array();
return array($subsql, $params);
}
////////////////////////////////////////////////////////////////////////////
/**
* 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 $gradinginfo 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
* @param boolean $cangrade whether current user has capability to grade in this context
* @return string
*/
public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) {
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));
*
* @param array $graderange
*/
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_NEEDUPDATE = 2;
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 $definitionid
* @param int $raterid
* @param int $itemid
* @return int id of the created instance
*/
public static function create_new($definitionid, $raterid, $itemid) {
global $DB;
$instance = new stdClass();
$instance->definitionid = $definitionid;
$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 current (active or needupdate) instance for the same raterid and itemid as this
* instance. This function is useful to find the status of the currently modified instance
*
* @return gradingform_instance
*/
public function get_current_instance() {
if ($this->get_status() == self::INSTANCE_STATUS_ACTIVE || $this->get_status() == self::INSTANCE_STATUS_NEEDUPDATE) {
return $this;
}
return $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid);
}
/**
* Returns the controller
*
* @return gradingform_controller
*/
public function get_controller() {
return $this->controller;
}
/**
* Returns the specified element from object $this->data
*
* @param string $key
* @return mixed
*/
public function get_data($key) {
if (isset($this->data->$key)) {
return $this->data->$key;
}
return null;
}
/**
* Returns instance id
*
* @return int
*/
public function get_id() {
return $this->get_data('id');
}
/**
* Returns instance status
*
* @return int
*/
public function get_status() {
return $this->get_data('status');
}
/**
* 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 && $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));
$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 '';
}
}

View File

@ -0,0 +1,117 @@
<?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/>.
/**
* Support for backup API
*
* @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();
/**
* Defines rubric backup structures
*/
class backup_gradingform_rubric_plugin extends backup_gradingform_plugin {
/**
* Declares rubric structures to append to the grading form definition
*/
protected function define_definition_plugin_structure() {
// Append data only if the grand-parent element has 'method' set to 'rubric'
$plugin = $this->get_plugin_element(null, '../../method', 'rubric');
// Create a visible container for our data
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect our visible container to the parent
$plugin->add_child($pluginwrapper);
// Define our elements
$criteria = new backup_nested_element('criteria');
$criterion = new backup_nested_element('criterion', array('id'), array(
'sortorder', 'description', 'descriptionformat'));
$levels = new backup_nested_element('levels');
$level = new backup_nested_element('level', array('id'), array(
'score', 'definition', 'definitionformat'));
// Build elements hierarchy
$pluginwrapper->add_child($criteria);
$criteria->add_child($criterion);
$criterion->add_child($levels);
$levels->add_child($level);
// Set sources to populate the data
$criterion->set_source_table('gradingform_rubric_criteria',
array('definitionid' => backup::VAR_PARENTID));
$level->set_source_table('gradingform_rubric_levels',
array('criterionid' => backup::VAR_PARENTID));
// no need to annotate ids or files yet (one day when criterion definition supports
// embedded files, they must be annotated here)
return $plugin;
}
/**
* Declares rubric structures to append to the grading form instances
*/
protected function define_instance_plugin_structure() {
// Append data only if the ancestor 'definition' element has 'method' set to 'rubric'
$plugin = $this->get_plugin_element(null, '../../../../method', 'rubric');
// Create a visible container for our data
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect our visible container to the parent
$plugin->add_child($pluginwrapper);
// Define our elements
$fillings = new backup_nested_element('fillings');
$filling = new backup_nested_element('filling', array('id'), array(
'criterionid', 'levelid', 'remark', 'remarkformat'));
// Build elements hierarchy
$pluginwrapper->add_child($fillings);
$fillings->add_child($filling);
// Set sources to populate the data
$filling->set_source_table('gradingform_rubric_fillings',
array('instanceid' => backup::VAR_PARENTID));
// no need to annotate ids or files yet (one day when remark field supports
// embedded fileds, they must be annotated here)
return $plugin;
}
}

View File

@ -0,0 +1,114 @@
<?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/>.
/**
* Support for restore API
*
* @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();
/**
* Restores the rubric specific data from grading.xml file
*/
class restore_gradingform_rubric_plugin extends restore_gradingform_plugin {
/**
* Declares the rubric XML paths attached to the form definition element
*
* @return array of {@link restore_path_element}
*/
protected function define_definition_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('gradingform_rubric_criterion',
$this->get_pathfor('/criteria/criterion'));
$paths[] = new restore_path_element('gradingform_rubric_level',
$this->get_pathfor('/criteria/criterion/levels/level'));
return $paths;
}
/**
* Declares the rubric XML paths attached to the form instance element
*
* @return array of {@link restore_path_element}
*/
protected function define_instance_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('gradinform_rubric_filling',
$this->get_pathfor('/fillings/filling'));
return $paths;
}
/**
* Processes criterion element data
*
* Sets the mapping 'gradingform_rubric_criterion' to be used later by
* {@link self::process_gradinform_rubric_filling()}
*/
public function process_gradingform_rubric_criterion($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->definitionid = $this->get_new_parentid('grading_definition');
$newid = $DB->insert_record('gradingform_rubric_criteria', $data);
$this->set_mapping('gradingform_rubric_criterion', $oldid, $newid);
}
/**
* Processes level element data
*
* Sets the mapping 'gradingform_rubric_level' to be used later by
* {@link self::process_gradinform_rubric_filling()}
*/
public function process_gradingform_rubric_level($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->criterionid = $this->get_new_parentid('gradingform_rubric_criterion');
$newid = $DB->insert_record('gradingform_rubric_levels', $data);
$this->set_mapping('gradingform_rubric_level', $oldid, $newid);
}
/**
* Processes filling element data
*/
public function process_gradinform_rubric_filling($data) {
global $DB;
$data = (object)$data;
$data->instanceid = $this->get_new_parentid('grading_instance');
$data->criterionid = $this->get_mappingid('gradingform_rubric_criterion', $data->criterionid);
$data->levelid = $this->get_mappingid('gradingform_rubric_level', $data->levelid);
$DB->insert_record('gradingform_rubric_fillings', $data);
}
}

View File

@ -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="definitionid"/>
<FIELD NAME="definitionid" 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="definitionid" 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_definitionid"/>
<KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" 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="instanceid"/>
<FIELD NAME="instanceid" 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="instanceid" 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_instanceid"/>
<KEY NAME="fk_instanceid" TYPE="foreign" FIELDS="instanceid" 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_instanceid" NEXT="uq_instance_criterion"/>
<KEY NAME="uq_instance_criterion" TYPE="unique" FIELDS="instanceid, 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>

View File

@ -0,0 +1,39 @@
<?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
*
* @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();
return true;
}

View File

@ -0,0 +1,62 @@
<?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'));
$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'allowdraft' => !$controller->has_active_instances()));
$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()) {
redirect($returnurl);
} else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) {
// everything ok, validated, re-grading confirmed if needed. Make changes to the rubric
$controller->update_definition($mform->get_data());
redirect($returnurl);
}
echo $OUTPUT->header();
$mform->display();
echo $OUTPUT->footer();

View File

@ -0,0 +1,194 @@
<?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 element 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 completion status
$choices = array();
$choices[gradingform_controller::DEFINITION_STATUS_DRAFT] = get_string('statusdraft', 'grading');
$choices[gradingform_controller::DEFINITION_STATUS_READY] = get_string('statusready', 'grading');
$form->addElement('select', 'status', get_string('rubricstatus', 'gradingform_rubric'), $choices)->freeze();
// rubric editor
$element = $form->addElement('rubriceditor', 'rubric', get_string('rubric', 'gradingform_rubric'));
$form->setType('rubric', PARAM_RAW);
//$element->freeze(); // TODO freeze rubric editor if needed
$buttonarray = array();
$buttonarray[] = &$form->createElement('submit', 'saverubric', get_string('saverubric', 'gradingform_rubric'));
if ($this->_customdata['allowdraft']) {
$buttonarray[] = &$form->createElement('submit', 'saverubricdraft', get_string('saverubricdraft', 'gradingform_rubric'));
}
$editbutton = &$form->createElement('submit', 'editrubric', ' ');
$editbutton->freeze();
$buttonarray[] = &$editbutton;
$buttonarray[] = &$form->createElement('cancel');
$form->addGroup($buttonarray, 'buttonar', '', array(' '), false);
$form->closeHeaderBefore('buttonar');
}
/**
* Form vlidation.
* If there are errors return array of errors ("fieldname"=>"error message"),
* otherwise true if ok.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
$err = parent::validation($data, $files);
$err = array();
$form = $this->_form;
$rubricel = $form->getElement('rubric');
if ($rubricel->non_js_button_pressed($data['rubric'])) {
// if JS is disabled and button such as 'Add criterion' is pressed - prevent from submit
$err['rubricdummy'] = 1;
} else if (isset($data['editrubric'])) {
// continue editing
$err['rubricdummy'] = 1;
} else if (isset($data['saverubric']) && $data['saverubric']) {
// If user attempts to make rubric active - it needs to be validated
if ($rubricel->validate($data['rubric']) !== false) {
$err['rubricdummy'] = 1;
}
}
return $err;
}
/**
* Return submitted data if properly submitted or returns NULL if validation fails or
* if there is no submitted data.
*
* @return object submitted data; NULL if not valid or not submitted or cancelled
*/
public function get_data() {
$data = parent::get_data();
if (!empty($data->saverubric)) {
$data->status = gradingform_controller::DEFINITION_STATUS_READY;
} else if (!empty($data->saverubricdraft)) {
$data->status = gradingform_controller::DEFINITION_STATUS_DRAFT;
}
return $data;
}
/**
* Check if there are changes in the rubric and it is needed to ask user whether to
* mark the current grades for re-grading. User may confirm re-grading and continue,
* return to editing or cancel the changes
*
* @param gradingform_rubric_controller $controller
*/
public function need_confirm_regrading($controller) {
$data = $this->get_data();
if (isset($data->rubric['regrade'])) {
// we have already displayed the confirmation on the previous step
return false;
}
if (!isset($data->saverubric) || !$data->saverubric) {
// we only need confirmation when button 'Save rubric' is pressed
return false;
}
if (!$controller->has_active_instances()) {
// nothing to re-grade, confirmation not needed
return false;
}
$changelevel = $controller->update_or_check_rubric($data);
if ($changelevel == 0) {
// no changes in the rubric, no confirmation needed
return false;
}
// freeze form elements and pass the values in hidden fields
// TODO description_editor does not freeze the normal way!
$form = $this->_form;
foreach (array('rubric', 'name'/*, 'description_editor'*/) as $fieldname) {
$el =& $form->getElement($fieldname);
$el->freeze();
$el->setPersistantFreeze(true);
if ($fieldname == 'rubric') {
$el->add_regrade_confirmation($changelevel);
}
}
// replace button text 'saverubric' and unfreeze 'Back to edit' button
$this->findButton('saverubric')->setValue(get_string('continue'));
$el =& $this->findButton('editrubric');
$el->setValue(get_string('backtoediting', 'gradingform_rubric'));
$el->unfreeze();
return true;
}
/**
* Returns a form element (submit button) with the name $elementname
*
* @param string $elementname
* @return HTML_QuickForm_element
*/
protected function &findButton($elementname) {
$form = $this->_form;
$buttonar =& $form->getElement('buttonar');
$elements =& $buttonar->getElements();
foreach ($elements as $el) {
if ($el->getName() == $elementname) {
return $el;
}
}
return null;
}
}

View File

@ -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]').get('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)
}
}

View File

@ -0,0 +1,226 @@
M.gradingform_rubriceditor = {'templates' : {}, 'eventhandler' : null, 'name' : null, 'Y' : null};
/**
* This function is called for each rubriceditor on page.
*/
M.gradingform_rubriceditor.init = function(Y, options) {
M.gradingform_rubriceditor.name = options.name
M.gradingform_rubriceditor.Y = Y
M.gradingform_rubriceditor.templates[options.name] = {
'criterion' : options.criteriontemplate,
'level' : options.leveltemplate
}
M.gradingform_rubriceditor.disablealleditors()
Y.on('click', M.gradingform_rubriceditor.clickanywhere, 'body', null)
M.gradingform_rubriceditor.addhandlers()
};
// Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html
M.gradingform_rubriceditor.addhandlers = function() {
var Y = M.gradingform_rubriceditor.Y
var name = M.gradingform_rubriceditor.name
if (M.gradingform_rubriceditor.eventhandler) M.gradingform_rubriceditor.eventhandler.detach()
M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubric-'+name+' input[type=submit]', null);
}
// switches all input text elements to non-edit mode
M.gradingform_rubriceditor.disablealleditors = function() {
var Y = M.gradingform_rubriceditor.Y
var name = M.gradingform_rubriceditor.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)} );
}
// function invoked on each click on the page. If level and/or criterion description is clicked
// it switches this element to edit mode. If rubric button is clicked it does nothing so the 'buttonclick'
// function is invoked
M.gradingform_rubriceditor.clickanywhere = function(e) {
var el = e.target
// if clicked on button - disablecurrenteditor, continue
if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') {
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
var focustb = false
while (el && !(el.hasClass('level') || el.hasClass('description'))) {
if (el.hasClass('score')) focustb = true
el = el.get('parentNode')
}
if (el) {
if (el.one('textarea').getStyle('display') == 'none') {
M.gradingform_rubriceditor.disablealleditors()
M.gradingform_rubriceditor.editmode(el, true, focustb)
}
return
}
// else disablecurrenteditor
M.gradingform_rubriceditor.disablealleditors()
}
// switch the criterion description or level to edit mode or switch back
M.gradingform_rubriceditor.editmode = function(el, editmode, focustb) {
var ta = el.one('textarea')
if (!editmode && ta.getStyle('display') == 'none') return;
if (editmode && ta.getStyle('display') == 'block') return;
var pseudotablink = '<a href="#" class="pseudotablink">&nbsp;</a>',
taplain = ta.get('parentNode').one('.plainvalue'),
tbplain = null,
tb = el.one('input[type=text]')
// add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable)
if (!taplain) {
ta.get('parentNode').append('<div class="plainvalue"><span class="textvalue">&nbsp;</span>'+pseudotablink+'</div>')
taplain = ta.get('parentNode').one('.plainvalue')
taplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere)
if (tb) {
tb.get('parentNode').append('<div class="plainvalue"><span class="textvalue">&nbsp;</span>'+pseudotablink+'</div>')
tbplain = tb.get('parentNode').one('.plainvalue')
tbplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere)
}
}
if (tb && !tbplain) tbplain = tb.get('parentNode').one('.plainvalue')
if (!editmode) {
// if we need to hide the input fields, copy their contents to plainvalue(s). If description/definition
// is empty, display the default text ('Click to edit ...') and add/remove 'empty' CSS class to element
var value = ta.get('value')
if (value.length) taplain.removeClass('empty')
else {
value = (el.hasClass('level')) ? M.str.gradingform_rubric.levelempty : M.str.gradingform_rubric.criterionempty
taplain.addClass('empty')
}
taplain.one('.textvalue').set('innerHTML', value)
if (tb) tbplain.one('.textvalue').set('innerHTML', tb.get('value'))
} else {
// if we need to show the input fields, set the width/height for textarea so it fills the cell
var width = parseFloat(ta.get('parentNode').getComputedStyle('width')),
height
if (el.hasClass('level')) height = parseFloat(el.getComputedStyle('height')) - parseFloat(el.one('.score').getComputedStyle('height'))
else height = parseFloat(ta.get('parentNode').getComputedStyle('height'))
ta.setStyle('width', Math.max(width,50)+'px').setStyle('height', Math.max(height,20)+'px')
}
// hide/display textarea, textbox and plaintexts
taplain.setStyle('display', editmode ? 'none' : 'block')
ta.setStyle('display', editmode ? 'block' : 'none')
if (tb) {
tbplain.setStyle('display', editmode ? 'none' : 'inline-block')
tb.setStyle('display', editmode ? 'inline-block' : 'none')
}
// focus the proper input field in edit mode
if (editmode) { if (tb && focustb) tb.focus(); else 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, confirmed) {
var Y = M.gradingform_rubriceditor.Y
var name = M.gradingform_rubriceditor.name
if (e.target.get('type') != 'submit') return;
M.gradingform_rubriceditor.disablealleditors()
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
if (action == 'addcriterion' || action == 'addlevel') {
var newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion')
var newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level')
}
var dialog_options = {
'scope' : this,
'callbackargs' : [e, true],
'callback' : M.gradingform_rubriceditor.buttonclick
};
if (chunks.length == 3 && action == 'addcriterion') {
// ADD NEW CRITERION
var levelsscores = [0], levidx = 1
var parentel = Y.one('#'+name+'-criteria')
if (parentel.one('>tbody')) parentel = parentel.one('>tbody')
if (parentel.all('.criterion').size()) {
var lastcriterion = parentel.all('.criterion').item(parentel.all('.criterion').size()-1).all('.level')
for (levidx=0;levidx<lastcriterion.size();levidx++) levelsscores[levidx] = lastcriterion.item(levidx).one('.score input[type=text]').get('value')
}
for (levidx;levidx<3;levidx++) levelsscores[levidx] = parseFloat(levelsscores[levidx-1])+1
var levelsstr = '';
for (levidx=0;levidx<levelsscores.length;levidx++) {
levelsstr += M.gradingform_rubriceditor.templates[name]['level'].replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)).replace(/\{LEVEL-score\}/g, levelsscores[levidx])
}
var newcriterion = M.gradingform_rubriceditor.templates[name]['criterion'].replace(/\{LEVELS\}/, levelsstr)
parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
M.gradingform_rubriceditor.assignclasses('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-levels .level')
M.gradingform_rubriceditor.addhandlers();
M.gradingform_rubriceditor.disablealleditors()
M.gradingform_rubriceditor.assignclasses(elements_str)
M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description'),true)
} else if (chunks.length == 5 && action == 'addlevel') {
// ADD NEW LEVEL
var newscore = 0;
parent = Y.one('#'+name+'-criteria-'+chunks[2]+'-levels')
parent.all('.level').each(function (node) { newscore = Math.max(newscore, parseFloat(node.one('.score input[type=text]').get('value'))+1) })
var newlevel = M.gradingform_rubriceditor.templates[name]['level'].
replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newlevid).replace(/\{LEVEL-score\}/g, newscore).replace(/\{.+?\}/g, '')
parent.append(newlevel)
M.gradingform_rubriceditor.addhandlers();
M.gradingform_rubriceditor.disablealleditors()
M.gradingform_rubriceditor.assignclasses(elements_str)
M.gradingform_rubriceditor.editmode(parent.all('.level').item(parent.all('.level').size()-1), true)
} 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())
M.gradingform_rubriceditor.assignclasses(elements_str)
} 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)
M.gradingform_rubriceditor.assignclasses(elements_str)
} else if (chunks.length == 4 && action == 'delete') {
// DELETE CRITERION
if (confirmed) {
Y.one('#'+name+'-criteria-'+chunks[2]).remove()
M.gradingform_rubriceditor.assignclasses(elements_str)
} 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()
M.gradingform_rubriceditor.assignclasses(elements_str)
} 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 (first/last/odd/even), level width and/or criterion sortorder for elements Y.all(elements_str)
M.gradingform_rubriceditor.assignclasses = function (elements_str) {
var elements = M.gradingform_rubriceditor.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)}
);
if (elements.item(i).hasClass('level')) elements.item(i).set('width', Math.round(100/elements.size())+'%')
}
}
// returns unique id for the next added element, it should not be equal to any of Y.all(elements_str) ids
M.gradingform_rubriceditor.calculatenewid = function (elements_str) {
var newid = 1
M.gradingform_rubriceditor.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);
} );
return newid
}

View File

@ -0,0 +1,71 @@
<?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['addcriterion'] = 'Add criterion';
$string['backtoediting'] = 'Back to editing';
$string['confirmdeletecriterion'] = 'Are you sure you want to delete this criterion?';
$string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
$string['criterionaddlevel'] = 'Add level';
$string['criteriondelete'] = 'Delete criterion';
$string['criterionempty'] = 'Click to edit criterion';
$string['criterionmovedown'] = 'Move down';
$string['criterionmoveup'] = 'Move up';
$string['definerubric'] = 'Define rubric';
$string['description'] = 'Description';
$string['enableremarks'] = 'Allow grader to add text remarks for each criteria';
$string['err_mintwolevels'] = 'Each criterion must have at least two levels';
$string['err_nocriteria'] = 'Rubric must contain at least one criterion';
$string['err_nodefinition'] = 'Level definition can not be empty';
$string['err_nodescription'] = 'Criterion description can not be empty';
$string['err_scoreformat'] = 'Number of points for each level must be a valid non-negative number';
$string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero';
$string['leveldelete'] = 'Delete level';
$string['levelempty'] = 'Click to edit level';
$string['name'] = 'Name';
$string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
$string['pluginname'] = 'Rubric';
$string['regrademessage1'] = 'You are about to save changes to a rubric that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the rubric will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
$string['regradeoption1'] = 'Mark for regrade';
$string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.';
$string['rubric'] = 'Rubric';
$string['rubricnotcompleted'] = 'Please choose something for each criterion';
$string['rubricoptions'] = 'Rubric options';
$string['rubricstatus'] = 'Current rubric status';
$string['saverubric'] = 'Save rubric and make it ready';
$string['saverubricdraft'] = 'Save as draft';
$string['scorepostfix'] = '{$a} points';
$string['showdescriptionstudent'] = 'Display rubric description to those being graded';
$string['showdescriptionteacher'] = 'Display rubric description during evaluation';
$string['showremarksstudent'] = 'Show remarks to those being graded';
$string['showscorestudent'] = 'Display points for each level to those being graded';
$string['showscoreteacher'] = 'Display points for each level during evaluation';
$string['sortlevelsasc'] = 'Sort order for levels:';
$string['sortlevelsasc0'] = 'Descending by number of points';
$string['sortlevelsasc1'] = 'Ascending by number of points';
$string['statusdraft'] = 'Draft';
$string['statusready'] = 'Ready';

View File

@ -0,0 +1,804 @@
<?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)
/** Rubric display mode: For editing (moderator or teacher creates a rubric) */
const DISPLAY_EDIT_FULL = 1;
/** Rubric display mode: Preview the rubric design with hidden fields */
const DISPLAY_EDIT_FROZEN = 2;
/** Rubric display mode: Preview the rubric design */
const DISPLAY_PREVIEW = 3;
/** Rubric display mode: For evaluation, enabled (teacher grades a student) */
const DISPLAY_EVAL = 4;
/** Rubric display mode: For evaluation, with hidden fields */
const DISPLAY_EVAL_FROZEN = 5;
/** Rubric display mode: Teacher reviews filled rubric */
const DISPLAY_REVIEW = 6;
/** Rubric display mode: Dispaly filled rubric (i.e. students see their grades) */
const DISPLAY_VIEW = 7;
/**
* 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) {
$this->update_or_check_rubric($newdefinition, $usermodified, true);
if (isset($newdefinition->rubric['regrade']) && $newdefinition->rubric['regrade']) {
$this->mark_for_regrade();
}
}
/**
* Either saves the rubric definition into the database or check if it has been changed.
* Returns the level of changes:
* 0 - no changes
* 1 - only texts or criteria sortorders are changed, students probably do not require re-grading
* 2 - added levels but maximum score on rubric is the same, students still may not require re-grading
* 3 - removed criteria or added levels or changed number of points, students require re-grading but may be re-graded automatically
* 4 - removed levels - students require re-grading and not all students may be re-graded automatically
* 5 - added criteria - all students require manual re-grading
*
* @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
* @param boolean $doupdate if true actually updates DB, otherwise performs a check
*
*/
public function update_or_check_rubric(stdClass $newdefinition, $usermodified = null, $doupdate = false) {
global $DB;
// firstly update the common definition data in the {grading_definition} table
if ($this->definition === false) {
if (!$doupdate) {
// if we create the new definition there is no such thing as re-grading anyway
return 5;
}
// if definition does not exist yet, create a blank one
// (we need id to save files embedded in description)
parent::update_definition(new stdClass(), $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(),
'grading', 'description', $this->definition->id);
// reload the definition from the database
$currentdefinition = $this->get_definition(true);
// update rubric data
$haschanges = array();
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'];
}
$criterionmaxscore = null;
if (preg_match('/^NEWID\d+$/', $id)) {
// insert criterion into DB
$data = array('definitionid' => $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];
}
}
if ($doupdate) {
$id = $DB->insert_record('gradingform_rubric_criteria', $data);
}
$haschanges[5] = 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;
if ($doupdate) {
$DB->update_record('gradingform_rubric_criteria', $data);
}
$haschanges[1] = true;
}
// remove deleted levels from DB and calculate the maximum score for this criteria
foreach ($currentcriteria[$id]['levels'] as $levelid => $currentlevel) {
if ($criterionmaxscore === null || $criterionmaxscore < $currentlevel['score']) {
$criterionmaxscore = $currentlevel['score'];
}
if (!array_key_exists($levelid, $levelsdata)) {
if ($doupdate) {
$DB->delete_records('gradingform_rubric_levels', array('id' => $levelid));
}
$haschanges[4] = true;
}
}
}
foreach ($levelsdata as $levelid => $level) {
if (isset($level['score'])) {
$level['score'] = (float)$level['score'];
if ($level['score']<0) {
// TODO why we can't allow negative score for rubric?
$level['score'] = 0;
}
}
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];
}
}
if ($doupdate) {
$levelid = $DB->insert_record('gradingform_rubric_levels', $data);
}
if ($criterionmaxscore !== null && $criterionmaxscore >= $level['score']) {
// new level is added but the maximum score for this criteria did not change, re-grading may not be necessary
$haschanges[2] = true;
} else {
$haschanges[3] = 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;
if ($doupdate) {
$DB->update_record('gradingform_rubric_levels', $data);
}
if (isset($data['score'])) {
$haschanges[3] = true;
}
$haschanges[1] = true;
}
}
}
}
// remove deleted criteria from DB
foreach (array_keys($currentcriteria) as $id) {
if (!array_key_exists($id, $newcriteria)) {
if ($doupdate) {
$DB->delete_records('gradingform_rubric_criteria', array('id' => $id));
$DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id));
}
$haschanges[3] = true;
}
}
foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) {
if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) {
$haschanges[1] = true;
}
}
if ($usermodified && $usermodified != $this->definition->usermodified) {
$haschanges[1] = true;
}
if (!count($haschanges)) {
return 0;
}
if ($doupdate) {
parent::update_definition($newdefinition, $usermodified);
$this->load_definition();
}
// return the maximum level of changes
$changelevels = array_keys($haschanges);
sort($changelevels);
return array_pop($changelevels);
}
/**
* Marks all instances filled with this rubric with the status INSTANCE_STATUS_NEEDUPDATE
*/
public function mark_for_regrade() {
global $DB;
if ($this->has_active_instances()) {
$conditions = array('definitionid' => $this->definition->id,
'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE);
$DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions);
}
}
/**
* 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.definitionid = 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', 'timecopied', '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) {
$value = $record->{'rl'.$fieldname};
if ($fieldname == 'score') {
$value = (float)$value; // To prevent display like 1.00000
}
$this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $value;
}
}
}
$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);
}
}
}
/**
* Returns the default options for the rubric display
*
* @return array
*/
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;
}
/**
* Gets the options of this rubric definition, fills the missing options with default values
*
* @return array
*/
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(),
'grading', '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;
}
/**
* Options for displaying the rubric description field in the form
*
* @param object $context
* @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,
);
}
/**
* Formats the definition description for display on page
*
* @return string
*/
public function get_formatted_description() {
if (!isset($this->definition->description)) {
return '';
}
$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,
'grading', 'description', $this->definition->id, $options);
$formatoptions = array(
'noclean' => false,
'trusted' => false,
'filter' => true,
'context' => $context
);
return format_text($description, $this->definition->descriptionformat, $formatoptions);
}
/**
* Returns the rubric plugin renderer
*
* @param moodle_page $page the target page
* @return gradingform_rubric_renderer
*/
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) {
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 = $this->get_renderer($page);
$criteria = $this->definition->rubric_criteria;
$options = $this->get_options();
$rubric = $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
return $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('definitionid' => $this->definition->id), '', 'id'));
// delete all fillings
$DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $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('definitionid' => $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);
}
/**
* If instanceid is specified and grading instance exists and it is created by this rater for
* this item, this instance is returned.
* If there exists a draft for this raterid+itemid, take this draft (this is the change from parent)
* Otherwise new instance is created for the specified rater and itemid
*
* @param int $instanceid
* @param int $raterid
* @param int $itemid
* @return gradingform_instance
*/
public function get_or_create_instance($instanceid, $raterid, $itemid) {
global $DB;
if ($instanceid &&
$instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
return $this->get_instance($instance);
}
if ($itemid && $raterid) {
if ($rs = $DB->get_records('grading_instances', array('raterid' => $raterid, 'itemid' => $itemid), 'timemodified DESC', '*', 0, 1)) {
$record = reset($rs);
$currentinstance = $this->get_current_instance($raterid, $itemid);
if ($record->status == gradingform_rubric_instance::INSTANCE_STATUS_INCOMPLETE &&
(!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) {
$record->isrestored = true;
return $this->get_instance($record);
}
}
}
return $this->create_instance($raterid, $itemid);
}
/**
* Returns html code to be included in student's feedback.
*
* @param moodle_page $page
* @param int $itemid
* @param array $gradinginfo result of function grade_get_grades
* @param string $defaultcontent default string to be returned if no active grading is found
* @param boolean $cangrade whether current user has capability to grade in this context
* @return string
*/
public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) {
return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade);
}
//// full-text search support /////////////////////////////////////////////
/**
* Prepare the part of the search query to append to the FROM statement
*
* @param string $gdid the alias of grading_definitions.id column used by the caller
* @return string
*/
public static function sql_search_from_tables($gdid) {
return " LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = $gdid)
LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)";
}
/**
* Prepare the parts of the SQL WHERE statement to search for the given token
*
* The returned array cosists of the list of SQL comparions and the list of
* respective parameters for the comparisons. The returned chunks will be joined
* with other conditions using the OR operator.
*
* @param string $token token to search for
* @return array
*/
public static function sql_search_where($token) {
global $DB;
$subsql = array();
$params = array();
// search in rubric criteria description
$subsql[] = $DB->sql_like('rc.description', '?', false, false);
$params[] = '%'.$DB->sql_like_escape($token).'%';
// search in rubric levels definition
$subsql[] = $DB->sql_like('rl.definition', '?', false, false);
$params[] = '%'.$DB->sql_like_escape($token).'%';
return array($subsql, $params);
}
}
/**
* 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('instanceid' => $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('instanceid' => $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
*
* @param array $elementvalue value of element as came in form submit
* @return boolean true if the form data is validated and contains no errors
*/
public function validate_grading_element($elementvalue) {
$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('instanceid' => $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('instanceid' => $this->get_id(), 'criterionid' => $criterionid,
'levelid' => $record['levelid'], 'remarkformat' => FORMAT_MOODLE);
if (isset($record['remark'])) {
$newrecord['remark'] = $record['remark'];
}
$DB->insert_record('gradingform_rubric_fillings', $newrecord);
} else {
$newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
foreach (array('levelid', 'remark'/*, 'remarkformat' TODO */) as $key) {
if (isset($record[$key]) && $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) {
$scores = array();
foreach ($criterion['levels'] as $level) {
$scores[] = $level['score'];
}
sort($scores);
$minscore += $scores[0];
$maxscore += $scores[sizeof($scores)-1];
}
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 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();
$html = '';
if ($value === null) {
$value = $this->get_rubric_filling();
} else if (!$this->validate_grading_element($value)) {
$html .= html_writer::tag('div', get_string('rubricnotcompleted', 'gradingform_rubric'), array('class' => 'gradingform_rubric-error'));
}
$currentinstance = $this->get_current_instance();
if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) {
$html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_rubric'), array('class' => 'gradingform_rubric-regrade'));
}
$haschanges = false;
if ($currentinstance) {
$curfilling = $currentinstance->get_rubric_filling();
foreach ($curfilling['criteria'] as $criterionid => $curvalues) {
$value['criteria'][$criterionid]['savedlevelid'] = $curvalues['levelid'];
$newremark = null;
$newlevelid = null;
if (isset($value['criteria'][$criterionid]['remark'])) $newremark = $value['criteria'][$criterionid]['remark'];
if (isset($value['criteria'][$criterionid]['levelid'])) $newlevelid = $value['criteria'][$criterionid]['levelid'];
if ($newlevelid != $curvalues['levelid'] || $newremark != $curvalues['remark']) {
$haschanges = true;
}
}
}
if ($this->get_data('isrestored') && $haschanges) {
$html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_rubric'), array('class' => 'gradingform_rubric-restored'));
}
$html .= $this->get_controller()->get_renderer($page)->display_rubric($criteria, $options, $mode, $gradingformelement->getName(), $value);
return $html;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

View File

@ -0,0 +1,456 @@
<?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 extends plugin_renderer_base {
/**
* 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 $levelsstr 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, $levelsstr = '{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] = '';
}
}
}
$criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], 'id' => '{NAME}-criteria-{CRITERION-id}'));
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$criteriontemplate .= 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, 'tabindex' => -1));
$criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
}
$criteriontemplate .= html_writer::end_tag('td'); // .controls
$criteriontemplate .= 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) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description']));
}
$description = $criterion['description'];
}
$descriptionclass = 'description';
if (isset($criterion['error_description'])) {
$descriptionclass .= ' error';
}
$criteriontemplate .= html_writer::tag('td', $description, array('class' => $descriptionclass, 'id' => '{NAME}-criteria-{CRITERION-id}-description'));
$levelsstrtable = html_writer::tag('table', html_writer::tag('tr', $levelsstr, array('id' => '{NAME}-criteria-{CRITERION-id}-levels')));
$levelsclass = 'levels';
if (isset($criterion['error_levels'])) {
$levelsclass .= ' error';
}
$criteriontemplate .= html_writer::tag('td', $levelsstrtable, array('class' => $levelsclass));
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));
$criteriontemplate .= 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'));
$criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
} else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
$criteriontemplate .= 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) {
$criteriontemplate .= html_writer::tag('td', $currentremark, array('class' => 'remark')); // TODO maybe some prefix here like 'Teacher remark:'
}
}
$criteriontemplate .= html_writer::end_tag('tr'); // .criterion
$criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate);
$criteriontemplate = str_replace('{CRITERION-id}', $criterion['id'], $criteriontemplate);
return $criteriontemplate;
}
/**
* 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']).'%';
}
$leveltemplate = html_writer::start_tag('td', $tdattributes);
$leveltemplate .= 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) {
$leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
$leveltemplate .= 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()));
$leveltemplate .= html_writer::tag('div', $input, array('class' => 'radio'));
}
if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
$leveltemplate .= 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'));
$definitionclass = 'definition';
if (isset($level['error_definition'])) {
$definitionclass .= ' error';
}
$leveltemplate .= html_writer::tag('div', $definition, array('class' => $definitionclass, '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) {
$scoreclass = 'score';
if (isset($level['error_score'])) {
$scoreclass .= ' error';
}
$leveltemplate .= html_writer::tag('div', get_string('scorepostfix', 'gradingform_rubric', $score), array('class' => $scoreclass));
}
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, 'tabindex' => -1));
$leveltemplate .= html_writer::tag('div', $button, array('class' => 'delete'));
}
$leveltemplate .= html_writer::end_tag('div'); // .level-wrapper
$leveltemplate .= html_writer::end_tag('td'); // .level
$leveltemplate = str_replace('{NAME}', $elementname, $leveltemplate);
$leveltemplate = str_replace('{CRITERION-id}', $criterionid, $leveltemplate);
$leveltemplate = str_replace('{LEVEL-id}', $level['id'], $leveltemplate);
return $leveltemplate;
}
/**
* 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 $criteriastr evaluated templates for this rubric's criteria
* @return string
*/
protected function rubric_template($mode, $options, $elementname, $criteriastr) {
$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;
}
$rubrictemplate = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix gradingform_rubric'.$classsuffix));
$rubrictemplate .= html_writer::tag('table', $criteriastr, 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));
$rubrictemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion'));
}
$rubrictemplate .= $this->rubric_edit_options($mode, $options);
$rubrictemplate .= html_writer::end_tag('div');
return str_replace('{NAME}', $elementname, $rubrictemplate);
}
/**
* Generates html template to view/edit the rubric options. Expression {NAME} is used in
* template for the form element name
*
* @param int $mode
* @param array $options
* @return string
*/
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'));
$valuestr = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id']));
$html .= html_writer::tag('span', $valuestr, 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:
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN && $value) {
$html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value));
}
// Display option as checkbox
$attrs['type'] = 'checkbox';
$attrs['value'] = 1;
if ($value) {
$attrs['checked'] = 'checked';
}
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN || $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) {
$criteriastr = '';
$cnt = 0;
foreach ($criteria as $id => $criterion) {
$criterion['class'] = $this->get_css_class_suffix($cnt++, sizeof($criteria) -1);
$criterion['id'] = $id;
$levelsstr = '';
$levelcnt = 0;
if (isset($values['criteria'][$id])) {
$criterionvalue = $values['criteria'][$id];
} else {
$criterionvalue = null;
}
foreach ($criterion['levels'] as $levelid => $level) {
$level['id'] = $levelid;
$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
}
if (isset($criterionvalue['savedlevelid']) && ((int)$criterionvalue['savedlevelid'] === $levelid)) {
$level['class'] .= ' currentchecked';
}
$level['tdwidth'] = 100/count($criterion['levels']);
$levelsstr .= $this->level_template($mode, $options, $elementname, $id, $level);
}
$criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $levelsstr, $criterionvalue);
}
return $this->rubric_template($mode, $options, $elementname, $criteriastr);
}
/**
* Help function to return CSS class names for element (first/last/even/odd) with leading space
*
* @param int $idx index of this element in the row/column
* @param int $maxidx maximum index of the element in the row/column
* @return string
*/
protected function get_css_class_suffix($idx, $maxidx) {
$class = '';
if ($idx == 0) {
$class .= ' first';
}
if ($idx == $maxidx) {
$class .= ' last';
}
if ($idx%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
* @param boolean $cangrade whether current user has capability to grade in this context
* @return string
*/
public function display_instances($instances, $defaultcontent, $cangrade) {
$return = '';
if (sizeof($instances)) {
$return .= html_writer::start_tag('div', array('class' => 'advancedgrade'));
$idx = 0;
foreach ($instances as $instance) {
$return .= $this->display_instance($instance, $idx++, $cangrade);
}
$return .= html_writer::end_tag('div');
}
return $return. $defaultcontent;
}
/**
* Displays one grading instance
*
* @param gradingform_rubric_instance $instance
* @param int idx unique number of instance on page
* @param boolean $cangrade whether current user has capability to grade in this context
*/
public function display_instance(gradingform_rubric_instance $instance, $idx, $cangrade) {
$criteria = $instance->get_controller()->get_definition()->rubric_criteria;
$options = $instance->get_controller()->get_options();
$values = $instance->get_rubric_filling();
if ($cangrade) {
$mode = gradingform_rubric_controller::DISPLAY_REVIEW;
} else {
$mode = gradingform_rubric_controller::DISPLAY_VIEW;
}
return $this->display_rubric($criteria, $options, $mode, 'rubric'.$idx, $values);
}
public function display_regrade_confirmation($elementname, $changelevel, $value) {
$html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade'));
if ($changelevel<=2) {
$html .= get_string('regrademessage1', 'gradingform_rubric');
$selectoptions = array(
0 => get_string('regradeoption0', 'gradingform_rubric'),
1 => get_string('regradeoption1', 'gradingform_rubric')
);
$html .= html_writer::select($selectoptions, $elementname.'[regrade]', $value, false);
} else {
$html .= get_string('regrademessage5', 'gradingform_rubric');
$html .= html_writer::empty_tag('input', array('name' => $elementname.'[regrade]', 'value' => 1, 'type' => 'hidden'));
}
$html .= html_writer::end_tag('div');
return $html;
}
}

View File

@ -0,0 +1,363 @@
<?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");
class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
/** help message */
public $_helpbutton = '';
/** stores the result of the last validation: null - undefined, false - no errors, string - error(s) text */
protected $validationerrors = null;
/** if element has already been validated **/
protected $wasvalidated = false;
/** If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
protected $nonjsbuttonpressed = false;
/** Message to display in front of the editor (that there exist grades on this rubric being edited) */
protected $regradeconfirmation = false;
function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) {
parent::HTML_QuickForm_input($elementName, $elementLabel, $attributes);
}
/**
* 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
*/
public 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
*/
public function getHelpButton() {
return $this->_helpbutton;
}
/**
* The renderer will take care itself about different display in normal and frozen states
*
* @return string
*/
public function getElementTemplateType() {
return 'default';
}
/**
* Specifies that confirmation about re-grading needs to be added to this rubric editor.
* $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
*
* @see gradingform_rubric_controller::update_or_check_rubric()
* @param int $changelevel
*/
public function add_regrade_confirmation($changelevel) {
$this->regradeconfirmation = $changelevel;
}
/**
* Returns html string to display this element
*
* @return string
*/
public function toHtml() {
global $PAGE;
$html = $this->_getTabs();
$renderer = $PAGE->get_renderer('gradingform_rubric');
$data = $this->prepare_data(null, $this->wasvalidated);
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;
}
}
if ($this->regradeconfirmation) {
if (!isset($data['regrade'])) {
$data['regrade'] = 1;
}
$html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
}
if ($this->validationerrors) {
$html .= $renderer->notification($this->validationerrors, 'error');
}
$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)
* sets $this->nonjsbuttonpressed to true/false if such button was pressed
* - 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
* - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
* and stores it in $this->validationerrors
*
* @param array $value
* @param boolean $withvalidation whether to enable data validation
* @return array
*/
protected function prepare_data($value = null, $withvalidation = false) {
if (null === $value) {
$value = $this->getValue();
}
if ($this->nonjsbuttonpressed === null) {
$this->nonjsbuttonpressed = false;
}
$totalscore = 0;
$errors = array();
$return = array('criteria' => array(), 'options' => gradingform_rubric_controller::get_default_options());
if (!isset($value['criteria'])) {
$value['criteria'] = array();
$errors['err_nocriteria'] = 1;
}
// If options are present in $value, replace default values with submitted values
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;
}
}
}
if (is_array($value)) {
// for other array keys of $value no special treatmeant neeeded, copy them to return value as is
foreach (array_keys($value) as $key) {
if ($key != 'options' && $key != 'criteria') {
$return[$key] = $value[$key];
}
}
}
// iterate through criteria
$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());
$i = 0;
// when adding new criterion copy the number of levels and their scores from the last criterion
if (!empty($value['criteria'][$lastid]['levels'])) {
foreach ($value['criteria'][$lastid]['levels'] as $lastlevel) {
$criterion['levels']['NEWID'+($i++)]['score'] = $lastlevel['score'];
}
} else {
$criterion['levels']['NEWID'+($i++)]['score'] = 0;
}
// add more levels so there are at least 3 in the new criterion. Increment by 1 the score for each next one
for ($i; $i<3; $i++) {
$criterion['levels']['NEWID'+$i]['score'] = $criterion['levels']['NEWID'+($i-1)]['score'] + 1;
}
// set other necessary fields (definition) for the levels in the new criterion
foreach (array_keys($criterion['levels']) as $i) {
$criterion['levels'][$i]['definition'] = '';
}
$this->nonjsbuttonpressed = true;
}
$levels = array();
$maxscore = null;
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,
);
foreach ($criterion['levels'] as $lastlevel) {
if ($level['score'] < $lastlevel['score'] + 1) {
$level['score'] = $lastlevel['score'] + 1;
}
}
$this->nonjsbuttonpressed = true;
}
if (!array_key_exists('delete', $level)) {
if ($withvalidation) {
if (!strlen(trim($level['definition']))) {
$errors['err_nodefinition'] = 1;
$level['error_definition'] = true;
}
if (!preg_match('#^[\+]?\d*$#', trim($level['score'])) && !preg_match('#^[\+]?\d*[\.,]\d+$#', trim($level['score']))) {
// TODO why we can't allow negative score for rubric?
$errors['err_scoreformat'] = 1;
$level['error_score'] = true;
}
}
$levels[$levelid] = $level;
if ($maxscore === null || (float)$level['score'] > $maxscore) {
$maxscore = (float)$level['score'];
}
} else {
$this->nonjsbuttonpressed = true;
}
}
}
$totalscore += (float)$maxscore;
$criterion['levels'] = $levels;
if ($withvalidation && !array_key_exists('delete', $criterion)) {
if (count($levels)<2) {
$errors['err_mintwolevels'] = 1;
$criterion['error_levels'] = true;
}
if (!strlen(trim($criterion['description']))) {
$errors['err_nodescription'] = 1;
$criterion['error_description'] = true;
}
}
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;
$this->nonjsbuttonpressed = true;
} else if (array_key_exists('delete', $criterion)) {
$this->nonjsbuttonpressed = true;
} else {
if (array_key_exists('movedown', $criterion)) {
unset($criterion['movedown']);
$lastaction = 'movedown';
$this->nonjsbuttonpressed = true;
}
$return['criteria'][$id] = $criterion;
$lastid = $id;
}
}
if ($totalscore <= 0) {
$errors['err_totalscore'] = 1;
}
// add sort order field to criteria
$csortorder = 1;
foreach (array_keys($return['criteria']) as $id) {
$return['criteria'][$id]['sortorder'] = $csortorder++;
}
// create validation error string (if needed)
if ($withvalidation) {
if (count($errors)) {
$rv = array();
foreach ($errors as $error => $v) {
$rv[] = get_string($error, 'gradingform_rubric');
}
$this->validationerrors = join('<br/ >', $rv);
} else {
$this->validationerrors = false;
}
$this->wasvalidated = true;
}
return $return;
}
/**
* Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
*
* @param array $ids
* @return string
*/
protected 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);
}
/**
* Checks if a submit button was pressed which is supposed to be processed on client side by JS
* but user seem to have disabled JS in the browser.
* (buttons 'add criteria', 'add level', 'move up', 'move down', etc.)
* In this case the form containing this element is prevented from being submitted
*
* @param array $value
* @return boolean true if non-submit button was pressed and not processed by JS
*/
public function non_js_button_pressed($value) {
if ($this->nonjsbuttonpressed === null) {
$this->prepare_data($value);
}
return $this->nonjsbuttonpressed;
}
/**
* Validates that rubric has at least one criterion, at least two levels within one criterion,
* each level has a valid score, all levels have filled definitions and all criteria
* have filled descriptions
*
* @param array $value
* @return string|false error text or false if no errors found
*/
public function validate($value) {
if (!$this->wasvalidated) {
$this->prepare_data($value, true);
}
return $this->validationerrors;
}
/**
* Prepares the data for saving
* @see prepare_data()
*
* @param array $submitValues
* @param boolean $assoc
* @return array
*/
public function exportValue(&$submitValues, $assoc = false) {
$value = $this->prepare_data($this->_findValue($submitValues));
return $this->_prepareValue($value, $assoc);
}
}

View File

@ -0,0 +1,119 @@
/*
.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.currentchecked {background:#fff0f0;}
.gradingform_rubric .criterion .levels .level.checked {background:#d0ffd0;border:1px solid #555;}
.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;}
.gradingform_rubric .criterion .levels.error { border:1px solid red;}
.gradingform_rubric .criterion .description.error,
.gradingform_rubric .criterion .levels .level .definition.error,
.gradingform_rubric .criterion .levels .level .score.error {background:#FFDDDD;}
/**
*
*/
.gradingform_rubric-regrade {padding:10px;background:#FFDDDD;border:1px solid #F00;margin-bottom:10px;}
.gradingform_rubric-restored {padding:10px;background:#FFFFDD;border:1px solid #FF0;margin-bottom:10px;}
.gradingform_rubric-error {color:red;font-weight:bold;}

View File

@ -0,0 +1,32 @@
<?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->component = 'gradingform_rubric';
$plugin->version = 2011110900;
$plugin->requires = 2011110200;
$plugin->maturity = MATURITY_STABLE;

651
grade/grading/lib.php Normal file
View File

@ -0,0 +1,651 @@
<?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
*
* There are basically ways how to use this factory method. If the area record
* id is known to the caller, get the manager for that area by providing just
* the id. If the area record id is not know, the context, component and area name
* can be provided. Note that null values are allowed in the second case as the context,
* component and the area name can be set explicitly later.
*
* @example $manager = get_grading_manager($areaid);
* @example $manager = get_grading_manager(get_system_context());
* @example $manager = get_grading_manager($context, 'mod_assignment', 'submission');
* @param stdClass|int|null $context or $areaid if $areaid is passed, no other parameter is needed
* @param string|null $component the frankenstyle name of the component
* @param string|null $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 installed grading plugins together, optionally extended
* with a simple direct grading.
*
* @param bool $includenone should the 'Simple direct grading' be included
* @return array of the (string)name => (string)localized title of the method
*/
public static function available_methods($includenone = true) {
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 available grading methods in the given context
*
* Currently this is just a static list obtained from {@link self::available_methods()}.
* 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'));
return self::available_methods($includenone);
}
/**
* Returns the list of gradable areas provided by the given component
*
* This performs a callback to the library of the relevant plugin to obtain
* the list of supported areas.
*
* @param string $component normalized component name
* @return array of (string)areacode => (string)localized title of the area
*/
public static function available_areas($component) {
global $CFG;
list($plugintype, $pluginname) = normalize_component($component);
if ($component === 'core_grading') {
return array();
} else if ($plugintype === 'mod') {
return plugin_callback('mod', $pluginname, 'grading', 'areas_list', null, array());
} else {
throw new coding_exception('Unsupported area location');
}
}
/**
* 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'));
if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
if ($this->get_component() !== 'core_grading') {
throw new coding_exception('Unsupported component at the system context');
} else {
return array();
}
} else if ($this->get_context()->contextlevel == CONTEXT_MODULE) {
list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
return self::available_areas('mod_'.$cm->modname);
} else {
throw new coding_exception('Unsupported gradable area context level');
}
}
/**
* 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' => context_system::instance()->id,
'component' => 'core_grading',
'areaname' => $name,
'activemethod' => $method);
return $DB->insert_record('grading_areas', $area);
}
/**
* Removes all data associated with the given context
*
* This is called by {@link context::delete_content()}
*
* @param int $contextid context id
*/
public static function delete_all_for_context($contextid) {
global $DB;
$areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid));
$methods = array_keys(self::available_methods(false));
foreach($areaids as $areaid) {
$manager = get_grading_manager($areaid);
foreach ($methods as $method) {
$controller = $manager->get_controller($method);
$controller->delete_definition();
}
}
$DB->delete_records_list('grading_areas', 'id', $areaids);
}
/**
* Helper method to tokenize the given string
*
* Splits the given string into smaller strings. This is a helper method for
* full text searching in grading forms. If the given string is surrounded with
* double quotes, the resulting array consists of a single item containing the
* quoted content.
*
* Otherwise, string like 'grammar, english language' would be tokenized into
* the three tokens 'grammar', 'english', 'language'.
*
* One-letter tokens like are dropped in non-phrase mode. Repeated tokens are
* returned just once.
*
* @param string $needle
* @return array
*/
public static function tokenize($needle) {
// check if we are searching for the exact phrase
if (preg_match('/^[\s]*"[\s]*(.*?)[\s]*"[\s]*$/', $needle, $matches)) {
$token = $matches[1];
if ($token === '') {
return array();
} else {
return array($token);
}
}
// split the needle into smaller parts separated by non-word characters
$tokens = preg_split("/\W/u", $needle);
// keep just non-empty parts
$tokens = array_filter($tokens);
// distinct
$tokens = array_unique($tokens);
// drop one-letter tokens
foreach ($tokens as $ix => $token) {
if (strlen($token) == 1) {
unset($tokens[$ix]);
}
}
return array_values($tokens);
}
////////////////////////////////////////////////////////////////////////////
/**
* 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.');
}
}
}
}

243
grade/grading/manage.php Normal file
View File

@ -0,0 +1,243 @@
<?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', context_system::instance());
$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));
$DB->set_field('grading_definitions', 'timecopied', time(), array('id' => $definition->id));
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();
// icon to edit the form definition
echo $output->management_action_icon($controller->get_editor_url($returnurl),
get_string('manageactionedit', 'core_grading'), 'b/document-edit');
// icon to delete the current form definition
echo $output->management_action_icon(new moodle_url($PAGE->url, array('deleteform' => $definition->id)),
get_string('manageactiondelete', 'core_grading'), 'b/edit-delete');
// icon to save the form as a new template
if (has_capability('moodle/grade:sharegradingforms', context_system::instance())) {
if (empty($definition->copiedfromid)) {
$hasoriginal = false;
} else {
$hasoriginal = $DB->record_exists('grading_definitions', array('id' => $definition->copiedfromid));
}
if (!$controller->is_form_available()) {
// drafts can not be shared
$allowshare = false;
} else if (!$hasoriginal) {
// was created from scratch or is orphaned
if (empty($definition->timecopied)) {
// was never shared before
$allowshare = true;
} else if ($definition->timemodified > $definition->timecopied) {
// was modified since last time shared
$allowshare = true;
} else {
// was not modified since last time shared
$allowshare = false;
}
} else {
// was created from a template and the template still exists
if ($definition->timecreated == $definition->timemodified) {
// was not modified since created
$allowshare = false;
} else if (empty($definition->timecopied)) {
// was modified but was not re-shared yet
$allowshare = true;
} else if ($definition->timemodified > $definition->timecopied) {
// was modified since last time re-shared
$allowshare = true;
} else {
// was not modified since last time re-shared
$allowshare = false;
}
}
if ($allowshare) {
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 message if the form is currently not available (if applicable)
if ($message = $controller->form_unavailable_notification()) {
echo $output->notification($message);
}
// display the grading form preview
if ($controller->is_form_defined()) {
if ($definition->status == gradingform_controller::DEFINITION_STATUS_READY) {
$tag = html_writer::tag('span', get_string('statusready', 'core_grading'), array('class' => 'status ready'));
} else {
$tag = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft'));
}
echo $output->heading(s($definition->name) . ' ' . $tag, 3, 'definition-name');
echo $output->box($controller->get_formatted_description());
echo $output->box($controller->render_preview($PAGE), 'definition-preview');
}
}
echo $output->footer();

263
grade/grading/pick.php Normal file
View File

@ -0,0 +1,263 @@
<?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/>.
/**
* Allows to choose a form from the list of available templates
*
* @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');
require_once($CFG->dirroot.'/grade/grading/pick_form.php');
$targetid = required_param('targetid', PARAM_INT); // area we are coming from
$pick = optional_param('pick', null, PARAM_INT); // create new form from this template
$remove = optional_param('remove', null, PARAM_INT); // remove this template
$confirmed = optional_param('confirmed', false, PARAM_BOOL); // is the action confirmed
// the manager of the target area
$targetmanager = get_grading_manager($targetid);
if ($targetmanager->get_context()->contextlevel < CONTEXT_COURSE) {
throw new coding_exception('Unsupported gradable area context level');
}
// currently active method in the target area
$method = $targetmanager->get_active_method();
$targetcontroller = $targetmanager->get_controller($method);
$targetcontrollerclass = get_class($targetcontroller);
// make sure there is no such form defined in the target area
if ($targetcontroller->is_form_defined()) {
redirect(new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid)));
}
list($context, $course, $cm) = get_context_info_array($targetmanager->get_context()->id);
require_login($course, true, $cm);
require_capability('moodle/grade:managegradingforms', $context);
// user's capability in the templates bank
$canshare = has_capability('moodle/grade:sharegradingforms', context_system::instance());
$canmanage = has_capability('moodle/grade:managesharedforms', context_system::instance());
// setup the page
$PAGE->set_url(new moodle_url('/grade/grading/pick.php', array('targetid' => $targetid)));
navigation_node::override_active_url($targetmanager->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 picking a template
if ($pick) {
$sourceid = $DB->get_field('grading_definitions', 'areaid', array('id' => $pick), MUST_EXIST);
$sourcemanager = get_grading_manager($sourceid);
$sourcecontroller = $sourcemanager->get_controller($method);
if (!$sourcecontroller->is_shared_template() and !$sourcecontroller->is_own_form()) {
// note that we don't actually check whether the user has still the capability
// moodle/grade:managegradingforms in the source area. so when users loose
// their teacher role in a course, they can't access the course but they can
// still copy the forms they have created there.
throw new moodle_exception('attempt_to_pick_others_form', 'core_grading');
}
if (!$sourcecontroller->is_form_defined()) {
throw new moodle_exception('form_definition_mismatch', 'core_grading');
}
$definition = $sourcecontroller->get_definition();
if (!$confirmed) {
echo $output->header();
echo $output->confirm(get_string('templatepickconfirm', 'core_grading',array(
'formname' => s($definition->name),
'component' => $targetmanager->get_component_title(),
'area' => $targetmanager->get_area_title())),
new moodle_url($PAGE->url, array('pick' => $pick, 'confirmed' => 1)),
$PAGE->url);
echo $output->box($sourcecontroller->render_preview($PAGE), 'template-preview-confirm');
echo $output->footer();
die();
} else {
require_sesskey();
$targetcontroller->update_definition($sourcecontroller->get_definition_copy($targetcontroller));
$DB->set_field('grading_definitions', 'timecopied', time(), array('id' => $definition->id));
redirect(new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid)));
}
}
// process removing a template
if ($remove) {
$sourceid = $DB->get_field('grading_definitions', 'areaid', array('id' => $remove), MUST_EXIST);
$sourcemanager = get_grading_manager($sourceid);
$sourcecontroller = $sourcemanager->get_controller($method);
if (!$sourcecontroller->is_shared_template()) {
throw new moodle_exception('attempt_to_delete_nontemplate', 'core_grading');
}
if (!$sourcecontroller->is_form_defined()) {
throw new moodle_exception('form_definition_mismatch', 'core_grading');
}
$definition = $sourcecontroller->get_definition();
if ($canmanage or ($canshare and ($definition->usercreated == $USER->id))) {
// ok, this user can drop the template
} else {
throw new moodle_exception('no_permission_to_remove_template', 'core_grading');
}
if (!$confirmed) {
echo $output->header();
echo $output->confirm(get_string('templatedeleteconfirm', 'core_grading', s($definition->name)),
new moodle_url($PAGE->url, array('remove' => $remove, 'confirmed' => 1)),
$PAGE->url);
echo $output->box($sourcecontroller->render_preview($PAGE), 'template-preview-confirm');
echo $output->footer();
die();
} else {
require_sesskey();
$sourcecontroller->delete_definition();
redirect($PAGE->url);
}
}
$searchform = new grading_search_template_form($PAGE->url, null, 'GET', '', array('class' => 'templatesearchform'));
if ($searchdata = $searchform->get_data()) {
$tokens = grading_manager::tokenize($searchdata->needle);
$includeownforms = (!empty($searchdata->mode));
} else {
$tokens = array();
$includeownforms = false;
}
// construct the SQL to find all matching templates
$sql = "SELECT DISTINCT gd.id, gd.areaid, gd.name, gd.description, gd.descriptionformat, gd.timecreated,
gd.usercreated, gd.timemodified, gd.usermodified
FROM {grading_definitions} gd
JOIN {grading_areas} ga ON (gd.areaid = ga.id)
JOIN {context} cx ON (ga.contextid = cx.id)";
// join method-specific tables from the plugin scope
$sql .= $targetcontrollerclass::sql_search_from_tables('gd.id');
$sql .= " WHERE gd.method = ?";
$params = array($method);
if (!$includeownforms) {
// search for public templates only
$sql .= " AND ga.contextid = ? AND ga.component = 'core_grading'";
$params[] = context_system::instance()->id;
} else {
// search both templates and own forms in other areas
$sql .= " AND ((ga.contextid = ? AND ga.component = 'core_grading')
OR (gd.usercreated = ? AND gd.status = ?))";
$params = array_merge($params, array(context_system::instance()->id, $USER->id,
gradingform_controller::DEFINITION_STATUS_READY));
}
if ($tokens) {
$subsql = array();
// search for any of the tokens in the definition name
foreach ($tokens as $token) {
$subsql[] = $DB->sql_like('gd.name', '?', false, false);
$params[] = '%'.$DB->sql_like_escape($token).'%';
}
// search for any of the tokens in the definition description
foreach ($tokens as $token) {
$subsql[] = $DB->sql_like('gd.description', '?', false, false);
$params[] = '%'.$DB->sql_like_escape($token).'%';
}
// search for the needle in method-specific tables
foreach ($tokens as $token) {
list($methodsql, $methodparams) = $targetcontrollerclass::sql_search_where($token);
$subsql = array_merge($subsql, $methodsql);
$params = array_merge($params, $methodparams);
}
$sql .= " AND ((" . join(")\n OR (", $subsql) . "))";
}
$sql .= " ORDER BY gd.name";
$rs = $DB->get_recordset_sql($sql, $params);
echo $output->header();
$searchform->display();
$found = 0;
foreach ($rs as $template) {
$found++;
$out = '';
$manager = get_grading_manager($template->areaid);
$controller = $manager->get_controller($method);
if ($controller->is_shared_template()) {
$templatetag = html_writer::tag('span', get_string('templatetypeshared', 'core_grading'),
array('class' => 'type shared'));
$templatesrc = '';
} else if ($controller->is_own_form()) {
$templatetag = html_writer::tag('span', get_string('templatetypeown', 'core_grading'),
array('class' => 'type ownform'));
$templatesrc = get_string('templatesource', 'core_grading', array(
'component' => $manager->get_component_title(),
'area' => $manager->get_area_title()));
} else {
throw new coding_exception('Something is wrong, the displayed form must be either template or own form');
}
$out .= $output->heading(s($template->name).' '.$templatetag, 2, 'template-name');
$out .= $output->container($templatesrc, 'template-source');
$out .= $output->box($controller->render_preview($PAGE), 'template-preview');
$actions = array();
if ($controller->is_shared_template()) {
$actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('pick' => $template->id)),
get_string('templatepick', 'core_grading'), 'i/tick_green_big', 'pick template');
if ($canmanage or ($canshare and ($template->usercreated == $USER->id))) {
//$actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('edit' => $template->id)),
// get_string('templateedit', 'core_grading'), 'i/edit', 'edit');
$actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('remove' => $template->id)),
get_string('templatedelete', 'core_grading'), 't/delete', 'remove');
}
} else if ($controller->is_own_form()) {
$actions[] = $output->pick_action_icon(new moodle_url($PAGE->url, array('pick' => $template->id)),
get_string('templatepickownform', 'core_grading'), 'i/tick_green_big', 'pick ownform');
}
$out .= $output->box(join(' ', $actions), 'template-actions');
$out .= $output->box($controller->get_formatted_description(), 'template-description');
// ideally we should highlight just the name, description and the fields
// in the preview that were actually searched. to make our life easier, we
// simply highlight the tokens everywhere they appear, even if that exact
// piece was not searched.
echo highlight(join(' ', $tokens), $out);
}
$rs->close();
if (!$found) {
echo $output->heading(get_string('nosharedformfound', 'core_grading'));
}
echo $output->single_button(
new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid)),
get_string('back'), 'get');
echo $output->footer();
////////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,51 @@
<?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/>.
/**
* Defines forms used by pick.php
*
* @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();
require_once($CFG->dirroot.'/lib/formslib.php');
/**
* Allows to search for a specific shared template
*/
class grading_search_template_form extends moodleform {
/**
* Pretty simple search box
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('header', 'searchheader', get_string('searchtemplate', 'core_grading'));
$mform->addHelpButton('searchheader', 'searchtemplate', 'core_grading');
$mform->addGroup(array(
$mform->createElement('checkbox', 'mode', '', get_string('searchownforms', 'core_grading')),
$mform->createElement('text', 'needle', '', array('size' => 30)),
$mform->createElement('submit', 'submitbutton', get_string('search')),
), 'buttonar', '', array(' '), false);
$mform->setType('needle', PARAM_TEXT);
$mform->setType('buttonar', PARAM_RAW);
}
}

View File

@ -0,0 +1,96 @@
<?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 template action icon
*
* @param moodle_url $url action URL
* @param string $text action text
* @param string $icon the name of the icon to use
* @param string $class extra class of this action
* @return string
*/
public function pick_action_icon(moodle_url $url, $text, $icon = '', $class = '') {
$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 '.$class));
}
}

View File

@ -0,0 +1,160 @@
<?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');
}
public function test_tokenize() {
$needle = " šašek, \n\n \r a král; \t";
$tokens = testable_grading_manager::tokenize($needle);
$this->assertEqual(2, count($tokens));
$this->assertTrue(in_array('šašek', $tokens));
$this->assertTrue(in_array('král', $tokens));
$needle = ' " šašek a král " ';
$tokens = testable_grading_manager::tokenize($needle);
$this->assertEqual(1, count($tokens));
$this->assertTrue(in_array('šašek a král', $tokens));
$needle = '""';
$tokens = testable_grading_manager::tokenize($needle);
$this->assertTrue(empty($tokens));
$needle = '"0"';
$tokens = testable_grading_manager::tokenize($needle);
$this->assertEqual(1, count($tokens));
$this->assertTrue(in_array('0', $tokens));
$needle = '<span>Aha</span>, then who\'s a bad guy here he?';
$tokens = testable_grading_manager::tokenize($needle);
$this->assertTrue(in_array('span', $tokens));
$this->assertTrue(in_array('Aha', $tokens));
$this->assertTrue(in_array('who', $tokens));
}
}

48
grade/grading/yui/manage/manage.js vendored Normal file
View File

@ -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'] });

View File

@ -1141,10 +1141,13 @@ class grade_structure {
private function get_activity_link($element) {
global $CFG;
/** @var array static cache of the grade.php file existence flags */
static $hasgradephp = array();
$itemtype = $element['object']->itemtype;
$itemmodule = $element['object']->itemmodule;
$iteminstance = $element['object']->iteminstance;
$itemnumber = $element['object']->itemnumber;
// Links only for module items that have valid instance, module and are
// called from grade_tree with valid modinfo
@ -1164,15 +1167,106 @@ class grade_structure {
return null;
}
if (!array_key_exists($itemmodule, $hasgradephp)) {
if (file_exists($CFG->dirroot . '/mod/' . $itemmodule . '/grade.php')) {
$hasgradephp[$itemmodule] = true;
} else {
$hasgradephp[$itemmodule] = false;
}
}
// If module has grade.php, link to that, otherwise view.php
$dir = $CFG->dirroot . '/mod/' . $itemmodule;
if (file_exists($dir.'/grade.php')) {
return new moodle_url('/mod/' . $itemmodule . '/grade.php', array('id' => $cm->id));
if ($hasgradephp[$itemmodule]) {
$args = array('id' => $cm->id, 'itemnumber' => $itemnumber);
if (isset($element['userid'])) {
$args['userid'] = $element['userid'];
}
return new moodle_url('/mod/' . $itemmodule . '/grade.php', $args);
} else {
return new moodle_url('/mod/' . $itemmodule . '/view.php', array('id' => $cm->id));
}
}
/**
* Returns URL of a page that is supposed to contain detailed grade analysis
*
* At the moment, only activity modules are supported. The method generates link
* to the module's file grade.php with the parameters id (cmid), itemid, itemnumber,
* gradeid and userid. If the grade.php does not exist, null is returned.
*
* @return moodle_url|null URL or null if unable to construct it
*/
public function get_grade_analysis_url(grade_grade $grade) {
global $CFG;
/** @var array static cache of the grade.php file existence flags */
static $hasgradephp = array();
if (empty($grade->grade_item) or !($grade->grade_item instanceof grade_item)) {
throw new coding_exception('Passed grade without the associated grade item');
}
$item = $grade->grade_item;
if (!$item->is_external_item()) {
// at the moment, only activity modules are supported
return null;
}
if ($item->itemtype !== 'mod') {
throw new coding_exception('Unknown external itemtype: '.$item->itemtype);
}
if (empty($item->iteminstance) or empty($item->itemmodule) or empty($this->modinfo)) {
return null;
}
if (!array_key_exists($item->itemmodule, $hasgradephp)) {
if (file_exists($CFG->dirroot . '/mod/' . $item->itemmodule . '/grade.php')) {
$hasgradephp[$item->itemmodule] = true;
} else {
$hasgradephp[$item->itemmodule] = false;
}
}
if (!$hasgradephp[$item->itemmodule]) {
return null;
}
$instances = $this->modinfo->get_instances();
if (empty($instances[$item->itemmodule][$item->iteminstance])) {
return null;
}
$cm = $instances[$item->itemmodule][$item->iteminstance];
if (!$cm->uservisible) {
return null;
}
$url = new moodle_url('/mod/'.$item->itemmodule.'/grade.php', array(
'id' => $cm->id,
'itemid' => $item->id,
'itemnumber' => $item->itemnumber,
'gradeid' => $grade->id,
'userid' => $grade->userid,
));
return $url;
}
/**
* Returns an action icon leading to the grade analysis page
*
* @param grade_grade $grade
* @return string
*/
public function get_grade_analysis_icon(grade_grade $grade) {
global $OUTPUT;
$url = $this->get_grade_analysis_url($grade);
if (is_null($url)) {
return '';
}
return $OUTPUT->action_icon($url, new pix_icon('t/preview',
get_string('gradeanalysis', 'core_grades')));
}
/**
* Returns the grade eid - the grade may not exist yet.
*

View File

@ -797,7 +797,7 @@ class grade_report_grader extends grade_report {
$rows = $this->get_right_icons_row($rows);
// Preload scale objects for items with a scaleid
// Preload scale objects for items with a scaleid and initialize tab indices
$scaleslist = array();
$tabindices = array();
@ -1003,6 +1003,9 @@ class grade_report_grader extends grade_report {
$itemcell->text .= html_writer::tag('span', get_string('error'), array('class'=>"gradingerror$hidden$gradepass"));
} else {
$itemcell->text .= html_writer::tag('span', grade_format_gradevalue($gradeval, $item, true, $gradedisplaytype, null), array('class'=>"gradevalue$hidden$gradepass"));
if ($this->get_pref('showanalysisicon')) {
$itemcell->text .= $this->gtree->get_grade_analysis_icon($grade);
}
}
}
@ -1435,7 +1438,7 @@ class grade_report_grader extends grade_report {
* figures out the state of the object and builds then returns a div
* with the icons needed for the grader report.
*
* @param object $object
* @param array $object
* @return string HTML
*/
protected function get_icons($element) {
@ -1457,7 +1460,6 @@ class grade_report_grader extends grade_report {
$lockunlockicon = '';
if (has_capability('moodle/grade:manage', $this->context)) {
if ($this->get_pref('showcalculations')) {
$editcalculationicon = $this->gtree->get_calculation_icon($element, $this->gpr);
}
@ -1469,9 +1471,15 @@ class grade_report_grader extends grade_report {
if ($this->get_pref('showlocks')) {
$lockunlockicon = $this->gtree->get_locking_icon($element, $this->gpr);
}
}
return $OUTPUT->container($editicon.$editcalculationicon.$showhideicon.$lockunlockicon, 'grade_icons');
$gradeanalysisicon = '';
if ($this->get_pref('showanalysisicon') && $element['type'] == 'grade') {
$gradeanalysisicon .= $this->gtree->get_grade_analysis_icon($element['object']);
}
return $OUTPUT->container($editicon.$editcalculationicon.$showhideicon.$lockunlockicon.$gradeanalysisicon, 'grade_icons');
}
/**

View File

@ -111,6 +111,7 @@ class grader_report_preferences_form extends moodleform {
$preferences['prefshow']['showuserimage'] = $checkbox_default;
$preferences['prefshow']['showactivityicons'] = $checkbox_default;
$preferences['prefshow']['showranges'] = $checkbox_default;
$preferences['prefshow']['showanalysisicon'] = $checkbox_default;
if ($canviewhidden) {
$preferences['prefrows']['shownumberofgrades'] = $checkbox_default;

View File

@ -60,6 +60,9 @@ if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configcheckbox('grade_report_showranges', get_string('showranges', 'grades'),
get_string('showranges_help', 'grades'), 0));
$settings->add(new admin_setting_configcheckbox('grade_report_showanalysisicon', get_string('showanalysisicon', 'core_grades'),
get_string('showanalysisicon_desc', 'core_grades'), 1));
$settings->add(new admin_setting_configcheckbox('grade_report_showuserimage', get_string('showuserimage', 'grades'),
get_string('showuserimage_help', 'grades'), 1));

View File

@ -471,6 +471,8 @@ width:2000px;
float:right;
}
.path-grade-report-grader .action-icon {margin-left:0.3em;}
.path-grade-report-grader .gradestable th.user,
.path-grade-report-grader .gradestable th.range,
.path-grade-report-grader .flexible th,

View File

@ -304,6 +304,7 @@ class grade_report_user extends grade_report {
$depth = $element['depth'];
$grade_object = $element['object'];
$eid = $grade_object->id;
$element['userid'] = $this->user->id;
$fullname = $this->gtree->get_element_header($element, true, true, true);
$data = array();
$hidden = '';

View File

@ -212,6 +212,7 @@ $string['fullview'] = 'Full view';
$string['generalsettings'] = 'General settings';
$string['grade'] = 'Grade';
$string['gradeadministration'] = 'Grade administration';
$string['gradeanalysis'] = 'Grade analysis';
$string['gradebook'] = 'Gradebook';
$string['gradebookhiddenerror'] = 'The gradebook is currently set to hide everything from students.';
$string['gradebookhistories'] = 'Grade histories';
@ -533,6 +534,9 @@ $string['setpreferences'] = 'Set preferences';
$string['setting'] = 'Setting';
$string['settings'] = 'Settings';
$string['setweights'] = 'Set weights';
$string['showanalysisicon'] = 'Show grade analysis icon';
$string['showanalysisicon_desc'] = 'Whether to show grade analysis icon by default. If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
$string['showanalysisicon_help'] = 'If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
$string['showaverage'] = 'Show average';
$string['showaverage_help'] = 'Show the average column? Students may be able to estimate other student\'s grades if the average is calculated from a small number of grades. For performance reasons the average is approximate if it is dependent on any hidden items.';
$string['showfeedback'] = 'Show feedback';

76
lang/en/grading.php Normal file
View File

@ -0,0 +1,76 @@
<?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['gradingformunavailable'] = 'Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status.';
$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 advanced 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'] = 'Delete the currently defined form';
$string['manageactiondeleteconfirm'] = 'You are going to delete 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 deleting 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.';
$string['manageactionsharedone'] = 'The form was successfully saved as a template';
$string['noitemid'] = 'Grading not possible. The graded item does not exist.';
$string['nosharedformfound'] = 'No template found';
$string['searchtemplate'] = 'Grading forms search';
$string['searchtemplate_help'] = 'You can search for a grading form and use it as a template for the new grading form here. Simply type words that should appear somewhere in the form name, its description or the form body itself. To search for a phrase, wrap the whole query in double quotes.
By default, only the grading forms that have been saved as shared templates are included in the search results. You can also include all your own grading forms in the search results. This way, you can simply re-use your grading forms without sharing them. Only forms marked as \'Ready for usage\' can be re-used this way.';
$string['searchownforms'] = 'include my own forms';
$string['statusdraft'] = 'Draft';
$string['statusready'] = 'Ready for usage';
$string['templatedelete'] = 'Delete';
$string['templatedeleteconfirm'] = 'You are going to delete the shared template \'{$a}\'. Deleting a template does not affect existing forms that were created from it.';
$string['templateedit'] = 'Edit';
$string['templatepick'] = 'Use this template';
$string['templatepickconfirm'] = 'Do you want to use the grading form \'{$a->formname}\' as a template for the new grading form in \'{$a->component} ({$a->area})\'?';
$string['templatepickownform'] = 'Use this form as a template';
$string['templatetypeown'] = 'Own form';
$string['templatetypeshared'] = 'Shared template';
$string['templatesource'] = 'Location: {$a->component} ({$a->area})';

View File

@ -77,6 +77,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';

View File

@ -180,6 +180,9 @@ $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:managesharedforms'] = 'Manage advanced grading form templates';
$string['grade:sharegradingforms'] = 'Share advanced grading form as a template';
$string['grade:manageletters'] = 'Manage letter grades';
$string['grade:manageoutcomes'] = 'Manage grade outcomes';
$string['grade:override'] = 'Override grades';

View File

@ -4902,6 +4902,10 @@ abstract class context extends stdClass {
$fs = get_file_storage();
$fs->delete_area_files($this->_id);
// delete all advanced grading data attached to this context
require_once($CFG->dirroot.'/grade/grading/lib.php');
grading_manager::delete_all_for_context($this->_id);
// now delete stuff from role related tables, role_unassign_all
// and unenrol should be called earlier to do proper cleanup
$DB->delete_records('role_assignments', array('contextid'=>$this->_id));

View File

@ -1501,6 +1501,41 @@ $capabilities = array(
'clonepermissionsfrom' => 'moodle/course:managegrades'
),
// ability to define advanced grading forms in activities either from scratch
// or from a shared template
'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'
),
// ability to save a grading form as a new shared template and eventually edit
// and remove own templates (templates originally shared by that user)
'moodle/grade:sharegradingforms' => array(
'riskbitmask' => RISK_XSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
),
// ability to edit and remove any shared template, even those originally shared
// by other users
'moodle/grade:managesharedforms' => array(
'riskbitmask' => RISK_XSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
),
'moodle/grade:manageoutcomes' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,

View 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="20111101" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -2771,7 +2771,7 @@
<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"/>
@ -2786,5 +2786,62 @@
<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="timecopied"/>
<FIELD NAME="timecopied" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" COMMENT="The timestamp of when this form was most recently copied into another area" PREVIOUS="usermodified" 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="timecopied"/>
</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="definitionid"/>
<FIELD NAME="definitionid" 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="definitionid" 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_definitionid"/>
<KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" REFTABLE="grading_definitions" REFFIELDS="id" PREVIOUS="primary" NEXT="fk_raterid"/>
<KEY NAME="fk_raterid" TYPE="foreign" FIELDS="raterid" REFTABLE="user" REFFIELDS="id" PREVIOUS="fk_definitionid"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>

View File

@ -6850,6 +6850,71 @@ FROM
upgrade_main_savepoint(true, 2011110200.01);
}
if ($oldversion < 2011110200.02) {
// create new core tables for the advanced grading methods framework
// grading_areas table
$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);
}
// grading_definitions 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('timecopied', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, '0');
$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);
}
// grading_instances 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('definitionid', 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_definitionid', XMLDB_KEY_FOREIGN, array('definitionid'), 'grading_definitions', array('id'));
$table->add_key('fk_raterid', XMLDB_KEY_FOREIGN, array('raterid'), 'user', array('id'));
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
upgrade_main_savepoint(true, 2011110200.02);
}
return true;
}

160
lib/form/grading.php Normal file
View File

@ -0,0 +1,160 @@
<?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;
/**
* Class constructor
*
* @param string $elementName Input field name attribute
* @param mixed $elementLabel Label(s) for the input field
* @param mixed $attributes Either a typical HTML attribute string or an associative array
* @return void
*/
public 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
*/
public 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
*/
public 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
*/
public 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
*/
public function getHelpButton(){
return $this->_helpbutton;
}
/**
* The renderer of gradingform_instance will take care itself about different display
* in normal and frozen states
*
* @return string
*/
public function getElementTemplateType(){
return 'default';
}
/**
* Called by HTML_QuickForm whenever form event is made on this element.
* Adds necessary rules to the element and checks that coorenct instance of gradingform_instance
* is passed in attributes
*
* @param string $event Name of event
* @param mixed $arg event arguments
* @param object $caller calling object
* @return void
* @throws moodle_exception
*/
public 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
*
* @param mixed $elementValue
* @param array $attributes
*/
static function _validate($elementValue, $attributes = null) {
return $attributes['gradinginstance']->validate_grading_element($elementValue);
}
}

View 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');

View 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');
@ -7352,6 +7354,7 @@ function get_core_subsystems() {
'fonts' => NULL,
'form' => 'lib/form',
'grades' => 'grade',
'grading' => 'grade/grading',
'group' => 'group',
'help' => NULL,
'hub' => NULL,
@ -7420,6 +7423,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',

View File

@ -3427,6 +3427,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;

View File

@ -366,6 +366,10 @@ class plugin_manager {
'grader', 'outcomes', 'overview', 'user'
),
'gradingform' => array(
'rubric'
),
'local' => array(
),

View File

@ -90,6 +90,7 @@ class restore_assignment_activity_structure_step extends restore_activity_struct
$newitemid = $DB->insert_record('assignment_submissions', $data);
$this->set_mapping('assignment_submission', $oldid, $newitemid, true); // Going to have files
$this->set_mapping(restore_gradingform_plugin::itemid_mapping('submission'), $oldid, $newitemid);
}
protected function after_execute() {

View File

@ -3,6 +3,7 @@
require_once("../../config.php");
$id = required_param('id', PARAM_INT); // Course module ID
$userid = optional_param('userid', 0, PARAM_INT); // Graded user ID (optional)
$PAGE->set_url('/mod/assignment/grade.php', array('id'=>$id));
if (! $cm = get_coursemodule_from_id('assignment', $id)) {
@ -20,7 +21,12 @@ if (! $course = $DB->get_record("course", array("id"=>$assignment->course))) {
require_login($course, false, $cm);
if (has_capability('mod/assignment:grade', get_context_instance(CONTEXT_MODULE, $cm->id))) {
redirect('submissions.php?id='.$cm->id);
if ($userid) {
redirect('submissions.php?id='.$cm->id.'&userid='.$userid.'&mode=single&filter=0&offset=0');
} else {
redirect('submissions.php?id='.$cm->id);
}
} else {
// user will view his own submission, parameter $userid is ignored
redirect('view.php?id='.$cm->id);
}

View File

@ -257,6 +257,8 @@ class assignment_base {
*
* This default method prints the teacher picture and name, date when marked,
* grade and teacher submissioncomment.
* If advanced grading is used the method render_grade from the
* advanced grading controller is called to display the grade.
*
* @global object
* @global object
@ -264,30 +266,31 @@ 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');
if (!is_enrolled($this->context, $USER, 'mod/assignment:view')) {
// can not submit assignments -> no feedback
return;
}
require_once("$CFG->dirroot/grade/grading/lib.php");
if (!$submission) { /// Get submission for this assignment
$submission = $this->get_submission($USER->id);
$userid = $USER->id;
$submission = $this->get_submission($userid);
} else {
$userid = $submission->userid;
}
// Check the user can submit
$cansubmit = has_capability('mod/assignment:submit', $this->context, $USER->id, false);
$canviewfeedback = ($userid == $USER->id && has_capability('mod/assignment:submit', $this->context, $USER->id, false));
// If not then check if the user still has the view cap and has a previous submission
$cansubmit = $cansubmit || (!empty($submission) && has_capability('mod/assignment:view', $this->context, $USER->id, false));
$canviewfeedback = $canviewfeedback || (!empty($submission) && $submission->userid == $USER->id && has_capability('mod/assignment:view', $this->context));
// Or if user can grade (is a teacher or admin)
$canviewfeedback = $canviewfeedback || has_capability('mod/assignment:grade', $this->context);
if (!$cansubmit) {
// can not submit assignments -> no feedback
if (!$canviewfeedback) {
// can not view or submit assignments -> no feedback
return;
}
$grading_info = grade_get_grades($this->course->id, 'mod', 'assignment', $this->assignment->id, $USER->id);
$grading_info = grade_get_grades($this->course->id, 'mod', 'assignment', $this->assignment->id, $userid);
$item = $grading_info->items[0];
$grade = $item->grades[$USER->id];
$grade = $item->grades[$userid];
if ($grade->hidden or $grade->grade === false) { // hidden or error
return;
@ -329,9 +332,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>';
$gradestr = '<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, $gradestr, has_capability('mod/assignment:grade', $this->context));
} else {
echo $gradestr;
}
echo '<div class="clearer"></div>';
echo '<div class="comment">';
@ -628,6 +635,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 +784,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 +815,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 +948,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);
}
@ -1039,13 +1070,34 @@ class assignment_base {
} elseif ($assignment->assignmenttype == 'uploadsingle') {
$mformdata->fileui_options = array('subdirs'=>0, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL);
}
$advancedgradingwarning = false;
$gradingmanager = get_grading_manager($this->context, 'mod_assignment', 'submission');
if ($gradingmethod = $gradingmanager->get_active_method()) {
$controller = $gradingmanager->get_controller($gradingmethod);
if ($controller->is_form_available()) {
$itemid = null;
if (!empty($submission->id)) {
$itemid = $submission->id;
}
if ($gradingdisabled && $itemid) {
$mformdata->advancedgradinginstance = $controller->get_current_instance($USER->id, $itemid);
} else if (!$gradingdisabled) {
$instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
$mformdata->advancedgradinginstance = $controller->get_or_create_instance($instanceid, $USER->id, $itemid);
}
} else {
$advancedgradingwarning = $controller->form_unavailable_notification();
}
}
$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;
}
@ -1064,6 +1116,9 @@ class assignment_base {
echo $OUTPUT->heading(get_string('feedback', 'assignment').': '.fullname($user, true));
// display mform here...
if ($advancedgradingwarning) {
echo $OUTPUT->notification($advancedgradingwarning, 'error');
}
$submitform->display();
$customfeedback = $this->custom_feedbackform($submission, true);
@ -1120,7 +1175,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);
@ -1362,6 +1417,8 @@ class assignment_base {
$locked_overridden = 'overridden';
}
// TODO add here code if advanced grading grade must be reviewed => $auser->status=0
$picture = $OUTPUT->user_picture($auser);
if (empty($auser->submissionid)) {
@ -1553,9 +1610,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'));
@ -1564,6 +1623,58 @@ 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.
*
* Note to the developers: This is NOT the correct way to implement advanced grading
* in grading form. The assignment grading was written long time ago and unfortunately
* does not fully use the mforms. Usually function is_validated() is called to
* validate the form and get_data() is called to get the data from the form.
*
* Here we have to push the calculated grade to $_POST['xgrade'] because further processing
* of the form gets the data not from form->get_data(), but from $_POST (using statement
* like $feedback = data_submitted() )
*/
protected function validate_and_preprocess_feedback() {
global $USER, $CFG;
require_once($CFG->libdir.'/gradelib.php');
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);
$gradinginfo = grade_get_grades($this->course->id, 'mod', 'assignment', $this->assignment->id, array($userid));
$gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked || $gradinginfo->items[0]->grades[$userid]->overridden;
if ($gradingdisabled) {
return true;
}
$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
*
@ -2029,12 +2140,6 @@ class assignment_base {
*/
function user_complete($user, $grade=null) {
global $OUTPUT;
if ($grade) {
echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
if ($grade->str_feedback) {
echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
}
}
if ($submission = $this->get_submission($user->id)) {
@ -2061,6 +2166,12 @@ class assignment_base {
echo $OUTPUT->box_end();
} else {
if ($grade) {
echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
if ($grade->str_feedback) {
echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
}
}
print_string("notsubmittedyet", "assignment");
}
}
@ -2241,11 +2352,17 @@ class assignment_base {
class mod_assignment_grading_form extends moodleform {
/** @var stores the advaned grading instance (if used in grading) */
private $advancegradinginstance;
function definition() {
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);
@ -2291,6 +2408,18 @@ class mod_assignment_grading_form extends moodleform {
}
/**
* 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;
@ -2299,20 +2428,33 @@ class mod_assignment_grading_form extends moodleform {
$attributes['disabled'] ='disabled';
}
$grademenu = make_grades_menu($this->_customdata->assignment->grade);
$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);
$grademenu = make_grades_menu($this->_customdata->assignment->grade);
if ($gradinginstance = $this->use_advanced_grading()) {
$gradinginstance->get_controller()->set_grade_range($grademenu);
$gradingelement = $mform->addElement('grading', 'advancedgrading', get_string('grade').':', array('gradinginstance' => $gradinginstance));
if ($this->_customdata->gradingdisabled) {
$gradingelement->freeze();
} else {
$mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
}
} else {
// use simple direct grading
$grademenu['-1'] = get_string('nograde');
$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) {
$options = make_grades_menu(-$outcome->scaleid);
if ($outcome->grades[$this->_customdata->submission->userid]->locked) {
$options[0] = get_string('nooutcome', 'grades');
echo $options[$outcome->grades[$this->_customdata->submission->userid]->grade];
$mform->addElement('static', 'outcome_'.$n.'['.$this->_customdata->userid.']', $outcome->name.':',
$options[$outcome->grades[$this->_customdata->submission->userid]->grade]);
} else {
$options[''] = get_string('nooutcome', 'grades');
$attributes = array('id' => 'menuoutcome_'.$n );
@ -2455,6 +2597,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;
}
}
@ -3682,7 +3829,7 @@ function assignment_reset_course_form_defaults($course) {
* Returns all other caps used in module
*/
function assignment_get_extra_capabilities() {
return array('moodle/site:accessallgroups', 'moodle/site:viewfullnames');
return array('moodle/site:accessallgroups', 'moodle/site:viewfullnames', 'moodle/grade:managegradingforms');
}
/**
@ -3701,6 +3848,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;
}
@ -3793,3 +3941,12 @@ function assignment_page_type_list($pagetype, $parentcontext, $currentcontext) {
);
return $module_pagetype;
}
/**
* Lists all gradable areas for the advanced grading methods gramework
*
* @return array
*/
function assignment_grading_areas_list() {
return array('submission' => get_string('submissions', 'mod_assignment'));
}

0
pix/b/BIG-ICONS Normal file
View File

BIN
pix/b/bookmark-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
pix/b/document-edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
pix/b/document-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
pix/b/edit-copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pix/b/edit-delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -654,6 +654,43 @@ if ($component === 'blog') {
question_pluginfile($course, $context, 'question', $filearea, $args, $forcedownload);
send_file_not_found();
// ========================================================================================================================
} else if ($component === 'grading') {
if ($filearea === 'description') {
// files embedded into the form definition description
if ($context->contextlevel == CONTEXT_SYSTEM) {
require_login();
} else if ($context->contextlevel >= CONTEXT_COURSE) {
require_login($course, false, $cm);
} else {
send_file_not_found();
}
$formid = (int)array_shift($args);
$sql = "SELECT ga.id
FROM {grading_areas} ga
JOIN {grading_definitions} gd ON (gd.areaid = ga.id)
WHERE gd.id = ? AND ga.contextid = ?";
$areaid = $DB->get_field_sql($sql, array($formid, $context->id), IGNORE_MISSING);
if (!$areaid) {
send_file_not_found();
}
$fullpath = "/$context->id/$component/$filearea/$formid/".implode('/', $args);
if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
send_file_not_found();
}
session_get_instance()->write_close(); // unlock session during fileserving
send_stored_file($file, 60*60, 0, $forcedownload);
}
// ========================================================================================================================
} else if (strpos($component, 'mod_') === 0) {
$modname = substr($component, 4);

View File

@ -32,4 +32,34 @@
/*adjustments to grader report (with static student cols) to make things line up in IE and Safari*/
#page-grade-report-grader-index .right_scroller #user-grades td {padding-top:0;padding-bottom:2px;}
#page-grade-report-grader-index #fixed_column td {padding-top:0;padding-bottom:2px;}
#page-grade-report-grader-index #fixed_column td {padding-top:0;padding-bottom:2px;}
/** 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%}
#page-grade-grading-manage .definition-name .status {font-weight:normal;text-transform:uppercase;font-size:60%;padding:0.25em;border:1px solid #EEE;-moz-border-radius:5px;}
#page-grade-grading-manage .definition-name .status.ready {background-color:#e7f1c3;border-color:#AAEEAA;}
#page-grade-grading-manage .definition-name .status.draft {background-color:#f3f2aa;border-color:#EEEE22;}
#page-grade-grading-manage .definition-preview {width:50%;margin:1em auto;border:1px solid #EEE; padding: 1em;}
#page-grade-grading-pick .templatesearchform {}
#page-grade-grading-pick .template-name {clear: both; padding:3px; background-color: #F6F6F6;}
#page-grade-grading-pick .template-name .type {font-weight:normal;text-transform:uppercase;font-size:60%;padding:0.25em;border:1px solid #EEE;-moz-border-radius:5px;}
#page-grade-grading-pick .template-name .type.shared {background-color:#e7f1c3;border-color:#AAEEAA}
#page-grade-grading-pick .template-name .type.ownform {background-color:#d2ebff;border-color:#AACCEE}
#page-grade-grading-pick .template-description {margin-bottom: 1em; padding: 0px 2em 0px 0px; margin-right:51%;}
#page-grade-grading-pick .template-preview {width:50%; float:right; border:1px solid #EEE; padding: 1em; margin-bottom: 1em;}
#page-grade-grading-pick .template-actions {margin-bottom: 1em; padding: 0px 2em 0px 0px; margin-right:51%;}
#page-grade-grading-pick .template-actions .action {display:inline-block;margin:0.25em;padding:0.25em;border:2px solid transparent;}
#page-grade-grading-pick .template-actions .action.pick {background-color:#EEE;border:2px solid #CCC;-moz-border-radius:3px}
#page-grade-grading-pick .template-actions .action:hover {text-decoration:none;background-color:#F6F6F6;border:2px solid #CCC;-moz-border-radius:3px}
#page-grade-grading-pick .template-actions .action .action-text {display:inline;}
#page-grade-grading-pick .template-actions .action .action-icon {margin:0px 3px;}
#page-grade-grading-pick .template-preview-confirm {width:50%;margin:1em auto;border:1px solid #EEE; padding: 1em;}
#page-grade-grading-pick .singlebutton {clear:both;}

View File

@ -33,4 +33,4 @@ 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;}
.gradetreebox tr .cell.level5 {background-color: #EBDEF6; width: 10px;}

View File

@ -30,7 +30,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2011110200.01; // YYYYMMDD = weekly release date of this DEV branch
$version = 2011110200.02; // YYYYMMDD = weekly release date of this DEV branch
// RR = release increments - 00 in DEV branches
// .XX = incremental changes