MDL-29794 Initial support for re-using a shared grading form
authorDavid Mudrak <david@moodle.com>
Tue, 1 Nov 2011 02:16:54 +0000 (03:16 +0100)
committerDavid Mudrak <david@moodle.com>
Tue, 1 Nov 2011 02:16:54 +0000 (03:16 +0100)
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
grade/grading/form/rubric/lib.php
grade/grading/lib.php
grade/grading/manage.php
grade/grading/renderer.php
grade/grading/simpletest/testlib.php
grade/grading/templates.php [new file with mode: 0644]
grade/grading/templates_form.php [new file with mode: 0644]
lang/en/grading.php
theme/standard/style/grade.css

index d66ae07..11d95da 100644 (file)
@@ -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);
+    }
+
     ////////////////////////////////////////////////////////////////////////////
 
     /**
index 63e08ac..2075039 100644 (file)
@@ -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);
+    }
 }
 
 /**
index ca36655..50f5af0 100644 (file)
@@ -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);
+    }
+
     ////////////////////////////////////////////////////////////////////////////
 
     /**
index 06ee3d1..5e80dcf 100644 (file)
@@ -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));
         }
index 526f4a5..1ab2955 100644 (file)
@@ -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));
     }
 }
index 1bb2a06..5c61118 100644 (file)
@@ -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 = '<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));
+    }
 }
diff --git a/grade/grading/templates.php b/grade/grading/templates.php
new file mode 100644 (file)
index 0000000..8a0ba15
--- /dev/null
@@ -0,0 +1,182 @@
+<?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/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 (file)
index 0000000..c0a32bf
--- /dev/null
@@ -0,0 +1,48 @@
+<?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 templates.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->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);
+    }
+}
index 12257df..21e9e2c 100644 (file)
@@ -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})\'?';
index 4fb48d5..3438746 100644 (file)
@@ -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;}
+