diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php index f1d0bfe2cb6..21d1f3f50bb 100644 --- a/grade/grading/form/lib.php +++ b/grade/grading/form/lib.php @@ -134,7 +134,7 @@ abstract class gradingform_controller { /** * Loads the form definition is it exists * - * The default implementation tries to load just the record ftom the {grading_definitions} + * The default implementation tries to load just 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. */ @@ -145,6 +145,56 @@ abstract class gradingform_controller { 'areaid' => $this->areaid, 'method' => $this->get_method_name()), '*', IGNORE_MISSING); } + + public function get_context() { + return $this->context; + } + + /** + * Updates the entry in table {grading_definitions} (or creates a new one if not present) + * + * The plugins are likely to override (extend) this with a more complex function that updates + * all changed data. + * + * @param $properties array or object of changed fields + * @param boolean $force specifies whether usermodified/timemodified should be explicitly updated even if $properties is empty + * @return boolean whether changes are made to DB + */ + public function update($properties, $force = false) { + global $DB, $USER; + if (!empty($properties)) { + $properties = (array)$properties; + } else { + $properties = array(); + } + + $data = array(); + foreach (array('name', 'description', 'descriptionformat', 'options', 'status') as $key) { + if (array_key_exists($key, $properties) && (!$this->definition || $this->definition->$key != $properties[$key])) { + $data[$key] = $properties[$key]; + } + } + if (!$this->definition || !empty($data) || $force) { + $data['timemodified'] = time(); + $data['usermodified'] = $USER->id; + if (!$this->definition) { + // Insert entry in DB + $data['areaid'] = $this->areaid; + $data['method'] = $this->get_method_name(); + if (!array_key_exists('descriptionformat', $data)) { + $data['descriptionformat'] = FORMAT_MOODLE; //TODO ? + } + $id = $DB->insert_record('grading_definitions', $data); + } else { + // Update entry in DB + $data['id'] = $this->definition->id; + $DB->update_record('grading_definitions', $data); + } + $this->load_definition(); + return true; + } + return false; + } } diff --git a/grade/grading/form/rubric/edit.php b/grade/grading/form/rubric/edit.php new file mode 100644 index 00000000000..da1b25b1eba --- /dev/null +++ b/grade/grading/form/rubric/edit.php @@ -0,0 +1,108 @@ +. + +/** + * @package gradingform + * @subpackage rubric + * @copyright 2011 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../../../../config.php"); +require_once("../../lib.php"); +require_once($CFG->dirroot.'/lib/formslib.php'); +require_once($CFG->dirroot."/grade/grading/form/rubric/rubriceditor.php"); +MoodleQuickForm::registerElementType('rubriceditor', $CFG->dirroot.'/grade/grading/form/rubric/rubriceditor.php', 'MoodleQuickForm_rubriceditor'); + +$areaid = required_param('areaid', PARAM_INT); // Area ID + +$manager = get_grading_manager($areaid); +$controller = $manager->get_controller('rubric'); + +$cm = $manager->get_cm(); +if (! $assignment = $DB->get_record("assignment", array("id"=>$cm->instance))) { + print_error('invalidid', 'assignment'); +} +if (! $course = $DB->get_record("course", array("id"=>$assignment->course))) { + print_error('coursemisconf', 'assignment'); +} + +require_login($course, false, $cm); +//require_capability('TODO', $context); // TODO + +$url = new moodle_url('/grade/grading/form/rubric/edit.php', array('areaid' => $areaid)); +$PAGE->set_url($url); +$title = get_string('definerubric', 'gradingform_rubric'); +$PAGE->set_title($title); +$PAGE->set_heading($title); + +$PAGE->requires->js('/grade/grading/form/rubric/js/rubriceditor.js'); + + + +class gradingform_rubric_editrubric extends moodleform { + function definition() { + $form = $this->_form; + + $form->addElement('hidden', 'areaid'); + $form->setType('areaid', PARAM_INT); + + // 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 = array();//gradingform_definition_base::description_form_field_options($this->_customdata['areaid']); + $form->addElement('editor', 'description_editor', get_string('description', 'gradingform_rubric'), null, $options); + $form->setType('description_editor', PARAM_RAW); + + // rubric editor + + $element = $form->addElement('rubriceditor', 'rubric', 'Rubric 1'); + $form->setType('rubric', PARAM_RAW); + $form->addRule('rubric', '', 'rubriceditorcompleted'); //TODO how to add this rule automatically????? + if (array_key_exists('freezerubric', $this->_customdata) && $this->_customdata['freezerubric']) { + $element->freeze(); + } + + $this->add_action_buttons(true); + } + + function set_data($obj) { + if ($obj instanceof gradingform_rubric_controller) { + $properties = $obj->get_data_for_edit(); + } else { + $properties = $obj; + } + parent::set_data($properties); + } +} + +//TODO freeze rubric editor if needed +$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'freezerubric' => optional_param('freeze', 0, PARAM_INT))); +$mform->set_data($controller); +if ($mform->is_submitted() && $mform->is_validated()) { + $data = $mform->get_data(); + $data = $controller->postupdate_data($data); + $controller->update($data); + redirect($url); +} + +echo $OUTPUT->header(); +$mform->display(); +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/grade/grading/form/rubric/js/rubriceditor.js b/grade/grading/form/rubric/js/rubriceditor.js new file mode 100644 index 00000000000..4ef9b15d44f --- /dev/null +++ b/grade/grading/form/rubric/js/rubriceditor.js @@ -0,0 +1,95 @@ +M.gradingform_rubriceditor = {'templates' : {}, 'eventhandler' : null}; + +/** + * This function is called for each rubriceditor on page. + */ +M.gradingform_rubriceditor.init = function(Y, options) { + M.gradingform_rubriceditor.templates[options.name] = { + 'criterion' : options.criteriontemplate, + 'level' : options.leveltemplate + } + M.gradingform_rubriceditor.addhandlers(Y, options.name); +}; + +// Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html +M.gradingform_rubriceditor.addhandlers = function(Y, name) { + if (M.gradingform_rubriceditor.eventhandler) M.gradingform_rubriceditor.eventhandler.detach() + M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubriceditor-'+name+' input[type=submit]', null, Y, name); +} + +// handler for clicking on submit buttons within rubriceditor element. Adds/deletes/rearranges criteria and/or levels on client side +M.gradingform_rubriceditor.buttonclick = function(e, Y, name, confirmed) { + if (e.target.get('type') != 'submit') return; + var chunks = e.target.get('id').split('-'), + action = chunks[chunks.length-1] + if (chunks[0] != name) return; + var elements_str + if (chunks.length>3 || action == 'addlevel') { + elements_str = '#rubriceditor-'+name+' #'+name+'-'+chunks[1]+'-levels .level' + } else { + elements_str = '#rubriceditor-'+name+' .criterion' + } + // prepare the id of the next inserted level or criterion + var newid = 1 + if (action == 'addcriterion' || action == 'addlevel') { + Y.all(elements_str).each( function(node) { + var idchunks = node.get('id').split('-'), id = idchunks.pop(); + if (id.match(/^NEWID(\d+)$/)) newid = Math.max(newid, parseInt(id.substring(5))+1); + } ); + } + var dialog_options = { + 'scope' : this, + 'callbackargs' : [e, Y, name, true], + 'callback' : M.gradingform_rubriceditor.buttonclick + }; + if (chunks.length == 2 && action == 'addcriterion') { + // ADD NEW CRITERION + var newcriterion = M.gradingform_rubriceditor.templates[name]['criterion']. + replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '') + Y.one('#'+name+'-criteria').append(newcriterion) + M.gradingform_rubriceditor.addhandlers(Y, name); + } else if (chunks.length == 3 && action == 'addlevel') { + // ADD NEW LEVEL + var newlevel = M.gradingform_rubriceditor.templates[name]['level']. + replace(/\{CRITERION-id\}/g, chunks[1]).replace(/\{LEVEL-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '') + Y.one('#'+name+'-'+chunks[1]+'-levels').append(newlevel) + M.gradingform_rubriceditor.addhandlers(Y, name); + } else if (chunks.length == 3 && action == 'moveup') { + // MOVE CRITERION UP + el = Y.one('#'+name+'-'+chunks[1]) + if (el.previous()) el.get('parentNode').insertBefore(el, el.previous()) + } else if (chunks.length == 3 && action == 'movedown') { + // MOVE CRITERION DOWN + el = Y.one('#'+name+'-'+chunks[1]) + if (el.next()) el.get('parentNode').insertBefore(el.next(), el) + } else if (chunks.length == 3 && action == 'delete') { + // DELETE CRITERION + if (confirmed) { + Y.one('#'+name+'-'+chunks[1]).remove() + } else { + dialog_options['message'] = M.str.gradingform_rubric.confirmdeletecriterion + M.util.show_confirm_dialog(e, dialog_options); + } + } else if (chunks.length == 5 && action == 'delete') { + // DELETE LEVEL + if (confirmed) { + Y.one('#'+name+'-'+chunks[1]+'-'+chunks[2]+'-'+chunks[3]).remove() + } else { + dialog_options['message'] = M.str.gradingform_rubric.confirmdeletelevel + M.util.show_confirm_dialog(e, dialog_options); + } + } else { + // unknown action + return; + } + e.preventDefault(); + // properly set classes and sortorder + var elements = Y.all(elements_str) + for (var i=0;idirroot.'/grade/grading/form/lib.php'); // parent class /** * This controller encapsulates the rubric grading logic */ -class rubric_grading_controller extends grading_controller { +class gradingform_rubric_controller extends gradingform_controller { + protected $_rubric; /** * Extends the module settings navigation with the rubric grading settings @@ -44,8 +45,226 @@ class rubric_grading_controller extends grading_controller { * @param navigation_node $node {@link navigation_node} */ public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) { + // TODO check permission to edit rubric $node->add(get_string('definerubric', 'gradingform_rubric'), - new moodle_url('/grade/grading/form/rubric/edit.php', array('area' => $this->areaid)), settings_navigation::TYPE_CUSTOM, + new moodle_url('/grade/grading/form/rubric/edit.php', array('areaid' => $this->areaid)), settings_navigation::TYPE_CUSTOM, null, null, new pix_icon('icon', '', 'gradingform_rubric')); } + + public function make_grading_widget($raterid, $itemid, $bulk = false) { + return new gradingform_rubric_widget(); + } + + protected function get_method_name() { + return 'rubric'; + } + + /** + * Updates DB with the changes + * + * @param $properties array or object of elements ($form->get_data()) + * @param boolean $force specifies whether usermodified/timemodified should be explicitly updated even if $properties is empty. Usually false for this class + */ + public function update($properties, $force = false) { + global $DB; + $parentmodified = parent::update($properties, $force); + + if (!empty($properties)) { + $properties = (array)$properties; + } else { + $properties = array(); + } + $haschanges = false; + if (array_key_exists('rubric', $properties)) { + $rubricdata = $properties['rubric']; + $rubric = $this->get_rubric(); + $criteriafields = array('sortorder', 'description', 'descriptionformat'); + $levelfields = array('score', 'definition', 'definitionformat'); + foreach ($rubricdata as $id => $criterion) { + // get list of submitted levels + $levelsdata = array(); + if (array_key_exists('levels', $criterion)) { + $levelsdata = $criterion['levels']; + } + if (preg_match('/^NEWID\d+$/', $id)) { + // insert criterion into DB + $data = array('formid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO format is not supported yet + foreach ($criteriafields as $key) { + if (array_key_exists($key, $criterion)) { + $data[$key] = $criterion[$key]; + } + } + $id = $DB->insert_record('gradingform_rubric_criteria', $data); + $haschanges = true; + } else { + // update criterion in DB + $data = array(); + foreach ($criteriafields as $key) { + if (array_key_exists($key, $criterion) && $criterion[$key] != $rubric[$id][$key]) { + $data[$key] = $criterion[$key]; + } + } + if (!empty($data)) { + // update only if something is changed + $data['id'] = $id; + $DB->update_record('gradingform_rubric_criteria', $data); + $haschanges = true; + } + // remove deleted levels from DB + foreach (array_keys($rubric[$id]['levels']) as $levelid) { + if (!array_key_exists($levelid, $levelsdata)) { + $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid)); + $haschanges = true; + } + } + } + foreach ($levelsdata as $levelid => $level) { + if (preg_match('/^NEWID\d+$/', $levelid)) { + // insert level into DB + $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO format is not supported yet + foreach ($levelfields as $key) { + if (array_key_exists($key, $level)) { + $data[$key] = $level[$key]; + } + } + $levelid = $DB->insert_record('gradingform_rubric_levels', $data); + $haschanges = true; + } else { + // update level in DB + $data = array(); + foreach ($levelfields as $key) { + if (array_key_exists($key, $level) && $level[$key] != $rubric[$id]['levels'][$levelid][$key]) { + $data[$key] = $level[$key]; + } + } + if (!empty($data)) { + // update only if something is changed + $data['id'] = $levelid; + $DB->update_record('gradingform_rubric_levels', $data); + $haschanges = true; + } + } + } + } + // remove deleted criteria from DB + foreach (array_keys($rubric) as $id) { + if (!array_key_exists($id, $rubricdata)) { + $DB->delete_records('gradingform_rubric_criteria', array('id' => $id)); + $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id)); + $haschanges = true; + } + } + $this->get_rubric($haschanges); + } + + // update time modified if was not updated before + if (!$parentmodified && ($force || $haschanges)) { + parent::update($properties, true); + } + } + + /** + * Returns the criteria/levels information stored in DB for current rubric + * + * @param boolean $force if true retrieves from DB even if cached result exists + * @return array + */ + public function get_rubric($force = false) { + global $DB; + if ($this->_rubric === null || $force) { + if (!$this->definition) { + return array(); // definition does not exist yet + } + $this->_rubric = array(); + $records = $DB->get_records('gradingform_rubric_criteria', array('formid' => $this->definition->id), 'sortorder, id'); + foreach ($records as $criterion) { + $this->_rubric[$criterion->id] = array( + 'description' => $criterion->description, + 'descriptionformat' => $criterion->descriptionformat, + 'sortorder' => $criterion->sortorder, + 'levels' => array(), + ); + $levels = $DB->get_records('gradingform_rubric_levels', array('criterionid' => $criterion->id), 'score, id'); // TODO sort order may be DESC! + foreach ($levels as $level) { + $this->_rubric[$criterion->id]['levels'][$level->id] = array( + 'definition' => $level->definition, + 'definitionformat' => $level->definitionformat, + 'score' => (float)$level->score, + ); + } + } + } + // TODO descriptionformat and definitionformat are not used at the moment + return $this->_rubric; + } + + /** + * returns an object for edit-rubric-form + * + * @return object + */ + public function get_data_for_edit() { + //TODO the following code may be moved to parent + $properties = new stdClass(); + if ($this->definition) { + foreach (array('id', 'name', 'description', 'descriptionformat', 'options', 'status') as $key) { + $properties->$key = $this->definition->$key; + } + } + $properties->areaid = $this->areaid; + if ($this->definition) { + $options = self::description_form_field_options($this->get_context()); + $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(), 'gradingform_rubric' /* TODO or gradingform? */, 'definition_description', $this->definition->id); + } + //TODO end // $properties = parent::get_data_for_edit(); + + $properties->rubric = $this->get_rubric(); + return $properties; + } + + // TODO the following functions may be moved to parent: + + public static function description_form_field_options($context) { + global $CFG; + return array( + 'maxfiles' => -1, + 'maxbytes' => get_max_upload_file_size($CFG->maxbytes), + 'context' => $context, + ); + } + + public function get_formatted_description() { + if (!$this->definition) { + return null; + } + $context = $this->get_context(); + + $options = self::description_form_field_options($this->get_context()); + $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id, 'gradingform_rubric' /* TODO or gradingform? */, 'definition_description', $this->definition->id, $options); + + $formatoptions = array( + 'noclean' => false, + 'trusted' => false, + 'filter' => true, + 'context' => $context + ); + return format_text($description, $this->definition->descriptionformat, $formatoptions); + } + + public function postupdate_data($data) { + if (!$this->definition) { + return $data; + } + $options = self::description_form_field_options($this->get_context()); + $data = file_postupdate_standard_editor($data, 'description', $options, $this->get_context(), 'gradingform_rubric' /* TODO or gradingform? */, 'definition_description', $this->definition->id); + // TODO change filearea for embedded files in grading_definition.description + return $data; + } } + +class gradingform_rubric_widget extends gradingform_widget { + + /** @var string unique identifier */ + public $id; + +} \ No newline at end of file diff --git a/grade/grading/form/rubric/renderer.php b/grade/grading/form/rubric/renderer.php new file mode 100644 index 00000000000..039823df1e4 --- /dev/null +++ b/grade/grading/form/rubric/renderer.php @@ -0,0 +1,46 @@ +. + +/** + * @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 gradingform_renderer { + + /** + * Renders grading widget + * + * @param gradingform_random_widget $widget + * @return string HTML + */ + protected function render_gradingform_rubric_widget(gradingform_rubric_widget $widget) { + + $button = html_writer::tag('button', 'Loading ...', array('type' => 'button', 'value' => 'Go')); + $span = html_writer::tag('span', ''); + + return $this->output->container($button.$span, 'gradingform_rubric-widget-wrapper', 1); + } +} diff --git a/grade/grading/form/rubric/rubriceditor.php b/grade/grading/form/rubric/rubriceditor.php new file mode 100644 index 00000000000..75a8db93f0f --- /dev/null +++ b/grade/grading/form/rubric/rubriceditor.php @@ -0,0 +1,268 @@ +. + +/** + * @package gradingform + * @subpackage rubric + * @copyright 2011 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("HTML/QuickForm/input.php"); + +// register file-related rules +if (class_exists('HTML_QuickForm')) { + HTML_QuickForm::registerRule('rubriceditorcompleted', 'callback', '_ruleIsCompleted', 'MoodleQuickForm_rubriceditor'); +} + +class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input { + public $_helpbutton = ''; + + function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) { + parent::HTML_QuickForm_input($elementName, $elementLabel, $attributes); + } + + function getHelpButton() { + return $this->_helpbutton; + } + + function getElementTemplateType() { + return 'default'; + } + + function toHtml() { + global $PAGE; + $html = $this->_getTabs(); + + // Template for the whole rubric editor + $classsuffix = $this->_flagFrozen ? 'frozen' : 'editable'; + $rubric_template = html_writer::start_tag('div', array('id' => 'rubriceditor-{NAME}', 'class' => 'clearfix form_rubric editor '.$classsuffix)); + $rubric_template .= html_writer::tag('div', '{CRITERIA}', array('class' => 'criteria', 'id' => '{NAME}-criteria')); + if (!$this->_flagFrozen) { + $rubric_template .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[addcriterion]', 'id' => '{NAME}-addcriterion', 'value' => get_string('addcriterion', 'gradingform_rubric'))); + } + $rubric_template .= html_writer::end_tag('div'); + + // Template for one criterion + $criterion_template = html_writer::start_tag('div', array('class' => 'clearfix criterion{CRITERION-class}', 'id' => '{NAME}-{CRITERION-id}')); + if (!$this->_flagFrozen) { + $criterion_template .= html_writer::start_tag('div', array('class' => 'controls')); + foreach (array('moveup', 'delete', 'movedown') as $key) { + $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[{CRITERION-id}]['.$key.']', + 'id' => '{NAME}-{CRITERION-id}-'.$key, 'value' => get_string('criterion'.$key, 'gradingform_rubric'))); + $criterion_template .= html_writer::tag('div', $button, array('class' => $key)); + } + $criterion_template .= html_writer::end_tag('div'); // .controls + $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][sortorder]', 'value' => '{CRITERION-sortorder}')); + $description = html_writer::tag('textarea', '{CRITERION-description}', array('name' => '{NAME}[{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5')); + } else { + if ($this->_persistantFreeze) { + $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][sortorder]', 'value' => '{CRITERION-sortorder}')); + $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][description]', 'value' => '{CRITERION-description}')); + } + $description = '{CRITERION-description}'; + } + $criterion_template .= html_writer::tag('div', $description, array('class' => 'description', 'id' => '{NAME}-{CRITERION-id}-description')); + $criterion_template .= html_writer::tag('div', '{LEVELS}', array('class' => 'levels', 'id' => '{NAME}-{CRITERION-id}-levels')); + if (!$this->_flagFrozen) { + $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[{CRITERION-id}][levels][addlevel]', + 'id' => '{NAME}-{CRITERION-id}-addlevel', 'value' => get_string('criterionaddlevel', 'gradingform_rubric'))); //TODO '{NAME}-{CRITERION-id}-levels-addlevel + $criterion_template .= html_writer::tag('div', $button, array('class' => 'addlevel')); + } + $criterion_template .= html_writer::end_tag('div'); // .criterion + + // Template for one level within one criterion + $level_template = html_writer::start_tag('div', array('id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}', 'class' => 'level{LEVEL-class}')); + if (!$this->_flagFrozen) { + $definition = html_writer::tag('textarea', '{LEVEL-definition}', array('name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4')); + $score = html_writer::empty_tag('input', array('type' => 'text', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '4', 'value' => '{LEVEL-score}')); + } else { + if ($this->_persistantFreeze) { + $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => '{LEVEL-definition}')); + $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][score]', 'value' => '{LEVEL-score}')); + } + $definition = '{LEVEL-definition}'; + $score = '{LEVEL-score}'; + } + $score = html_writer::tag('span', $score, array('id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-score')); + $level_template .= html_writer::tag('div', $definition, array('class' => 'definition', 'id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-definition')); + $level_template .= html_writer::tag('div', $score. get_string('scorepostfix', 'gradingform_rubric'), array('class' => 'score')); + if (!$this->_flagFrozen) { + $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][delete]', 'id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-delete', 'value' => get_string('leveldelete', 'gradingform_rubric'))); + $level_template .= html_writer::tag('div', $button, array('class' => 'delete')); + } + $level_template .= html_writer::end_tag('div'); // .level + + $criterion_template = str_replace('{NAME}', $this->getName(), $criterion_template); + $level_template = str_replace('{NAME}', $this->getName(), $level_template); + $rubric_template = str_replace('{NAME}', $this->getName(), $rubric_template); + + if (!$this->_flagFrozen) { + $module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js', + 'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'))); + $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(array('name' => $this->getName(), 'criteriontemplate' => $criterion_template, 'leveltemplate' => $level_template)), true, $module); + } + $rubric_html = $rubric_template; + $criteria = $this->prepare_non_js_data(); + $cnt = 0; + foreach ($criteria as $id => $criterion) { + $criterion_html = $criterion_template; + $levelcnt = 0; + foreach ($criterion['levels'] as $levelid => $level) { + $cell_html = $level_template; + $cell_html = str_replace('{LEVEL-id}', $levelid, $cell_html); + $cell_html = str_replace('{LEVEL-definition}', htmlspecialchars($level['definition']), $cell_html); + $cell_html = str_replace('{LEVEL-score}', htmlspecialchars($level['score']), $cell_html); + $cell_html = str_replace('{LEVEL-class}', $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1), $cell_html); + $criterion_html = str_replace('{LEVELS}', $cell_html.'{LEVELS}', $criterion_html); + } + $criterion_html = str_replace('{LEVELS}', '', $criterion_html); + $criterion_html = str_replace('{CRITERION-id}', $id, $criterion_html); + $criterion_html = str_replace('{CRITERION-description}', htmlspecialchars($criterion['description']), $criterion_html); + $criterion_html = str_replace('{CRITERION-sortorder}', htmlspecialchars($criterion['sortorder']), $criterion_html); + $criterion_html = str_replace('{CRITERION-class}', $this->get_css_class_suffix($cnt++, sizeof($criteria) -1), $criterion_html); + $rubric_html = str_replace('{CRITERIA}', $criterion_html.'{CRITERIA}', $rubric_html); + } + $rubric_html = str_replace('{CRITERIA}', '', $rubric_html); + $html .= $rubric_html; + + return $html; + } + + function get_css_class_suffix($cnt, $maxcnt) { + $class = ''; + if ($cnt == 0) { + $class .= ' first'; + } + if ($cnt == $maxcnt) { + $class .= ' last'; + } + if ($cnt%2) { + $class .= ' odd'; + } else { + $class .= ' even'; + } + return $class; + } + + function prepare_non_js_data() { + $return = array(); + $criteria = $this->getValue(); + if (empty($criteria)) { + $criteria = array(); + } + $lastaction = null; + $lastid = null; + foreach ($criteria as $id => $criterion) { + if ($id == 'addcriterion') { + $id = $this->get_next_id(array_keys($criteria)); + $criterion = array('description' => ''); + } + $levels = array(); + if (array_key_exists('levels', $criterion)) { + foreach ($criterion['levels'] as $levelid => $level) { + if ($levelid == 'addlevel') { + $levelid = $this->get_next_id(array_keys($criterion['levels'])); + $level = array( + 'definition' => '', + 'score' => 0, + ); + } + if (!array_key_exists('delete', $level)) { + $levels[$levelid] = $level; + } + } + } + $criterion['levels'] = $levels; + if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') { + unset($criterion['moveup']); + if ($lastid !== null) { + $lastcriterion = $return[$lastid]; + unset($return[$lastid]); + $return[$id] = $criterion; + $return[$lastid] = $lastcriterion; + } else { + $return[$id] = $criterion; + } + $lastaction = null; + $lastid = $id; + } else if (array_key_exists('delete', $criterion)) { + } else { + if (array_key_exists('movedown', $criterion)) { + unset($criterion['movedown']); + $lastaction = 'movedown'; + } + $return[$id] = $criterion; + $lastid = $id; + } + } + $csortorder = 1; + foreach (array_keys($return) as $id) { + $return[$id]['sortorder'] = $csortorder++; + } + return $return; + } + + function get_next_id($ids) { + $maxid = 0; + foreach ($ids as $id) { + if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) { + $maxid = (int)$matches[1]; + } + } + return 'NEWID'.($maxid+1); + } + + function _ruleIsCompleted($elementValue) { + //echo "_ruleIsCompleted"; + if (is_array($elementValue)) { + foreach ($elementValue as $criterionid => $criterion) { + if ($criterionid == 'addcriterion') { + return false; + } + if (array_key_exists('moveup', $criterion) || array_key_exists('movedown', $criterion) || array_key_exists('delete', $criterion)) { + return false; + } + if (array_key_exists('levels', $criterion) && is_array($criterion['levels'])) { + foreach ($criterion['levels'] as $levelid => $level) { + if ($levelid == 'addlevel') { + return false; + } + if (array_key_exists('delete', $level)) { + return false; + } + } + } + } + } + //TODO check everything is filled + //echo "
";print_r($elementValue);echo "
"; + return true; + } + + function onQuickFormEvent($event, $arg, &$caller) + { + $name = $this->getName(); + if ($name && $caller->elementExists($name)) { + $caller->addRule($name, '', 'rubriceditorcompleted'); + } + return parent::onQuickFormEvent($event, $arg, $caller); + } + +} \ No newline at end of file diff --git a/grade/grading/form/rubric/styles.css b/grade/grading/form/rubric/styles.css new file mode 100644 index 00000000000..d0822ad01e8 --- /dev/null +++ b/grade/grading/form/rubric/styles.css @@ -0,0 +1,66 @@ +/* +.form_rubric.editor[.frozen|.editable] + .criterion[.first][.last].odd/even + .controls + .moveup + [input type=submit] + .delete + [input type=submit] + .movedown + [input type=submit] + .description + .levels + .level[.first][.last].odd/even + .definition + [textarea] + .score + span + [input type=text] + .delete + [input type=submit] + .addlevel + [input type=submit] + .addcriterion + [input type=submit] + +.form_rubric.evaluate + .criterion[.first][.last].odd/even + .description + .levels + .level[.first][.last].odd/even + .definition + .score + span + +*/ + + +.form_rubric.editor .criterion .controls, +.form_rubric .criterion .description, +.form_rubric .criterion .levels, +.form_rubric.editor .criterion .addlevel, +.form_rubric .criterion .levels .level {display: inline-block; vertical-align: top;} + +.form_rubric.editor .criterion .controls, +.form_rubric .criterion .description, +.form_rubric.editor .criterion .addlevel, +.form_rubric .criterion .levels .level {padding:3px} + +.form_rubric .criterion {border:1px solid #DDD;} +.form_rubric .criterion.even {background:#F0F0F0;} + +.form_rubric .criterion .description {min-width:100px;font-weight:bold;} + +.form_rubric .criterion .levels .level {border-left:1px solid #DDD;min-width:80px;} +.form_rubric .criterion .levels .level.last {border-right:1px solid #DDD;} + +.form_rubric .criterion .levels .level .score {display:inline-block;} +.form_rubric.editor .criterion .levels .level .delete {display:inline-block; padding-left: 5px;} + +/* Make invisible the buttons 'Move up' for the first criterion and 'Move down' for the last, because those buttons will make no change */ +.form_rubric.editor .criterion.first .controls .moveup input, +.form_rubric.editor .criterion.last .controls .movedown input {display:none;} + +/* evaluation */ +.form_rubric.evaluate .criterion .levels .level.checked {background:#d0ffd0;} +.form_rubric.evaluate .criterion .levels .level:hover {background:#30ff30;} diff --git a/grade/grading/lib.php b/grade/grading/lib.php index 3c3446c65ef..ebd7125408e 100644 --- a/grade/grading/lib.php +++ b/grade/grading/lib.php @@ -101,6 +101,17 @@ class grading_manager { $this->context = $context; } + /** + * Returns the current context + */ + public function get_context() { + return $this->context; + } + + public function get_cm() { + return get_coursemodule_from_id(null, $this->context->instanceid); + } + /** * Sets the component the manager operates on *