From 20836db99924d57598c7ca685b66d9629a998af5 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Tue, 1 Nov 2011 03:16:54 +0100 Subject: [PATCH] MDL-29794 Initial support for re-using a shared grading form The patch introduces a new script templates.php that allows to search for a previously shared form (template) and re-use it. The patch also modifies the preview rendering of grading forms. Now plugins are responsible for rendering the form itselft, without any headers, descriptions etc (we need to embed the form preview into various places so the caller looks after the frame). --- grade/grading/form/lib.php | 42 +++++-- grade/grading/form/rubric/lib.php | 48 ++++++- grade/grading/lib.php | 45 +++++++ grade/grading/manage.php | 2 +- grade/grading/renderer.php | 15 ++- grade/grading/simpletest/testlib.php | 29 +++++ grade/grading/templates.php | 182 +++++++++++++++++++++++++++ grade/grading/templates_form.php | 48 +++++++ lang/en/grading.php | 4 + theme/standard/style/grade.css | 12 ++ 10 files changed, 405 insertions(+), 22 deletions(-) create mode 100644 grade/grading/templates.php create mode 100644 grade/grading/templates_form.php diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php index d66ae077c4e..11d95dae710 100644 --- a/grade/grading/form/lib.php +++ b/grade/grading/form/lib.php @@ -382,22 +382,13 @@ abstract class gradingform_controller { /** * Returns the HTML code displaying the preview of the grading form * - * Plugins are supposed to override/extend this. Ideally they should delegate + * 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 */ - public function render_preview(moodle_page $page) { - - if (!$this->is_form_defined()) { - throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); - } - - $output = $page->get_renderer('core_grading'); - - return $output->preview_definition_header($this); - } + abstract public function render_preview(moodle_page $page); /** * Deletes the form definition and all the associated data @@ -423,6 +414,35 @@ abstract class gradingform_controller { $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) { + global $DB; + + $subsql = array(); + $params = array(); + + return array($subsql, $params); + } + //////////////////////////////////////////////////////////////////////////// /** diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index 63e08acf084..2075039081b 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -366,16 +366,16 @@ class gradingform_rubric_controller extends gradingform_controller { */ public function render_preview(moodle_page $page) { - // use the parent's method to render the common information about the form - $header = parent::render_preview($page); + if (!$this->is_form_defined()) { + throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); + } - // append the rubric itself, using own renderer $output = $this->get_renderer($page); $criteria = $this->definition->rubric_criteria; $options = $this->get_options(); $rubric = $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric'); - return $header . $rubric; + return $rubric; } /** @@ -411,6 +411,46 @@ class gradingform_rubric_controller extends gradingform_controller { $instances = $this->get_current_instances($itemid); return $this->get_renderer($page)->display_instances($this->get_current_instances($itemid), $defaultcontent); } + + //// 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.formid = $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); + } } /** diff --git a/grade/grading/lib.php b/grade/grading/lib.php index ca36655c43b..50f5af068f3 100644 --- a/grade/grading/lib.php +++ b/grade/grading/lib.php @@ -514,6 +514,51 @@ class grading_manager { return $DB->insert_record('grading_areas', $area); } + /** + * 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); + } + //////////////////////////////////////////////////////////////////////////// /** diff --git a/grade/grading/manage.php b/grade/grading/manage.php index 06ee3d1da01..5e80dcfddd6 100644 --- a/grade/grading/manage.php +++ b/grade/grading/manage.php @@ -171,7 +171,7 @@ if (!empty($method)) { } 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())); + $pickurl = new moodle_url('/grade/grading/templates.php', array('targetid' => $controller->get_areaid())); if (!is_null($returnurl)) { $pickurl->param('returnurl', $returnurl->out(false)); } diff --git a/grade/grading/renderer.php b/grade/grading/renderer.php index 526f4a5e704..1ab295599d5 100644 --- a/grade/grading/renderer.php +++ b/grade/grading/renderer.php @@ -79,15 +79,18 @@ class core_grading_renderer extends plugin_renderer_base { } /** - * Renders the common information about the form definition + * Renders the template action icon * - * @param gradingform_controller $controller + * @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 preview_definition_header(gradingform_controller $controller) { + public function pick_action_icon(moodle_url $url, $text, $icon = '', $class = '') { - $definition = $controller->get_definition(); - // todo make this nicer, append the information about the time created/modified etc - return $this->output->heading(format_text($definition->name)); + $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)); } } diff --git a/grade/grading/simpletest/testlib.php b/grade/grading/simpletest/testlib.php index 1bb2a067dbf..5c6111873a1 100644 --- a/grade/grading/simpletest/testlib.php +++ b/grade/grading/simpletest/testlib.php @@ -128,4 +128,33 @@ class grading_manager_test extends UnitTestCase { $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 = 'Aha, 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)); + } } diff --git a/grade/grading/templates.php b/grade/grading/templates.php new file mode 100644 index 00000000000..8a0ba15c2eb --- /dev/null +++ b/grade/grading/templates.php @@ -0,0 +1,182 @@ +. + +/** + * Allows to choose a form from the list of available templates + * + * @package core + * @subpackage grading + * @copyright 2011 David Mudrak + * @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/templates_form.php'); + +$targetid = required_param('targetid', PARAM_INT); // area we are coming from +$pick = optional_param('pick', null, PARAM_INT); // use this form +$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()) { + throw new moodle_exception('target_defined', 'core_grading'); +} + +list($context, $course, $cm) = get_context_info_array($targetmanager->get_context()->id); + +require_login($course, true, $cm); +require_capability('moodle/grade:managegradingforms', $context); + +$PAGE->set_url(new moodle_url('/grade/grading/templates.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 template actions +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_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)); + redirect(new moodle_url('/grade/grading/manage.php', array('areaid' => $targetid))); + } +} + +$searchform = new grading_search_template_form($PAGE->url, null, 'GET', '', array('class' => 'templatesearchform')); + +if ($searchdata = $searchform->get_data()) { + $needle = $searchdata->needle; + $searchform->set_data(array( + 'needle' => $needle, + )); +} else { + $needle = ''; +} + +// construct the SQL to find all matching templates +$sql = "SELECT DISTINCT gd.id, gd.areaid, gd.name, gd.description, gd.descriptionformat, gd.timecreated + FROM {grading_definitions} gd + JOIN {grading_areas} ga ON (gd.areaid = ga.id)"; +// join method-specific tables from the plugin scope +$sql .= $targetcontrollerclass::sql_search_from_tables('gd.id'); + +$sql .= " WHERE gd.method = ? + AND ga.contextid = ? + AND ga.component = 'core_grading'"; + +$params = array($method, get_system_context()->id); + +$tokens = grading_manager::tokenize($needle); +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 = ''; + $out .= $output->heading(s($template->name), 2, 'template-name'); + $manager = get_grading_manager($template->areaid); + $controller = $manager->get_controller($method); + $out .= $output->box($controller->render_preview($PAGE), 'template-preview'); + $out .= $output->box(join(' ', array( + $output->pick_action_icon(new moodle_url($PAGE->url, array('pick' => $template->id)), + get_string('templatepick', 'core_grading'), 'i/tick_green_big', 'pick'), + //$output->pick_action_icon(new moodle_url($PAGE->url, array('edit' => $template->id)), + // get_string('templateedit', 'core_grading'), 'i/edit', 'edit'), + //$output->pick_action_icon(new moodle_url($PAGE->url, array('remove' => $template->id)), + // get_string('templatedelete', 'core_grading'), 't/delete', 'edit'), + )), 'template-actions'); + $out .= $output->box(format_text($template->description, $template->descriptionformat), '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('nothingtodisplay')); +} + +echo $output->footer(); + +//////////////////////////////////////////////////////////////////////////////// + + diff --git a/grade/grading/templates_form.php b/grade/grading/templates_form.php new file mode 100644 index 00000000000..c0a32bff33a --- /dev/null +++ b/grade/grading/templates_form.php @@ -0,0 +1,48 @@ +. + +/** + * Defines forms used by templates.php + * + * @package core + * @subpackage grading + * @copyright 2011 David Mudrak + * @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->addGroup(array( + $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); + } +} diff --git a/lang/en/grading.php b/lang/en/grading.php index 12257dfeeac..21e9e2c7ced 100644 --- a/lang/en/grading.php +++ b/lang/en/grading.php @@ -56,3 +56,7 @@ $string['manageactionshare'] = 'Publish the form as a new template'; $string['manageactionshareconfirm'] = 'You are going to save a copy of the grading form \'{$a}\' as a new public template. Other users at your site will be able to create new grading forms in their activities from that template. Note that users are able to reuse their own grading forms in other activities even if the forms were not saved as template.'; $string['manageactionsharedone'] = 'The form was successfully saved as a template'; $string['noitemid'] = 'Grading not possible. The graded item does not exist.'; +$string['templatedelete'] = 'Remove'; +$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})\'?'; diff --git a/theme/standard/style/grade.css b/theme/standard/style/grade.css index 4fb48d57469..34387468972 100644 --- a/theme/standard/style/grade.css +++ b/theme/standard/style/grade.css @@ -45,3 +45,15 @@ td.grade div.overridden {background-color: #DDDDDD;} #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-templates .templatesearchform {text-align:center;margin:0px auto;} +#page-grade-grading-templates .template-name {clear: both; padding:3px; background-color: #F6F6F6;} +#page-grade-grading-templates .template-description {margin-bottom: 1em; padding: 0px 2em 0px 0px; margin-right:51%;} +#page-grade-grading-templates .template-preview {width:50%; float:right; border:1px solid #EEE; padding: 1em; margin-bottom: 1em;} +#page-grade-grading-templates .template-actions {margin-bottom: 1em; padding: 0px 2em 0px 0px; margin-right:51%;} +#page-grade-grading-templates .template-actions .action {display:inline-block;margin:0.25em;padding:0.25em;border:2px solid transparent;} +#page-grade-grading-templates .template-actions .action.pick {background-color:#EEE;border:2px solid #CCC;-moz-border-radius:3px} +#page-grade-grading-templates .template-actions .action:hover {text-decoration:none;background-color:#F6F6F6;border:2px solid #CCC;-moz-border-radius:3px} +#page-grade-grading-templates .template-actions .action .action-text {display:inline;} +#page-grade-grading-templates .template-actions .action .action-icon {margin:0px 3px;} +#page-grade-grading-templates .template-preview-confirm {width:50%;margin:1em auto;border:1px solid #EEE; padding: 1em;} + -- 2.43.0