MDL-20636 Essay questions can now handle files in the HTML editor. #216

This commit is contained in:
Tim Hunt
2011-03-31 12:45:36 +01:00
parent 7a3e1f7d51
commit 48d9c17db3
5 changed files with 336 additions and 106 deletions
+29 -15
View File
@@ -119,15 +119,15 @@ class question_engine_data_mapper {
$record->id = $this->db->insert_record('question_attempt_steps', $record);
foreach ($step->get_all_data() as $name => $value) {
if ($value instanceof question_file_saver) {
$value->save_files($record->id, $context);
}
$data = new stdClass();
$data->attemptstepid = $record->id;
$data->name = $name;
$data->value = $value;
$this->db->insert_record('question_attempt_step_data', $data, false);
if ($value instanceof question_file_saver) {
$value->save_files($record->id, $context);
}
}
}
@@ -1022,24 +1022,27 @@ class question_file_saver {
* @param string $component the component for the file area to save into.
* @param string $filearea the name of the file area to save into.
*/
public function __construct($draftitemid, $component, $filearea) {
public function __construct($draftitemid, $component, $filearea, $text = null) {
$this->draftitemid = $draftitemid;
$this->component = $component;
$this->filearea = $filearea;
$this->value = $this->compute_value($draftitemid, $text);
}
protected function get_value() {
/**
* Compute the value that should be stored in the question_attempt_step_data
* table. Contains a hash that (almost) uniquely encodes all the files.
* @param int $draftitemid the draft file area itemid.
* @param string $text optional content containing file links.
*/
protected function compute_value($draftitemid, $text) {
global $USER;
if (!is_null($this->value)) {
return $this->value;
}
$fs = get_file_storage();
$usercontext = get_context_instance(CONTEXT_USER, $USER->id);
$files = $fs->get_area_files($usercontext->id, 'user', 'draft',
$this->draftitemid, 'sortorder, filepath, filename', false);
$draftitemid, 'sortorder, filepath, filename', false);
$string = '';
foreach ($files as $file) {
@@ -1048,16 +1051,27 @@ class question_file_saver {
}
if ($string) {
$this->value = md5($string);
$hash = md5($string);
} else {
$this->value = '';
$hash = '';
}
return $this->value;
if (is_null($text)) {
return $hash;
}
// We add the file hash so a simple string comparison will say if the
// files have been changed. First strip off any existing file hash.
$text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
$text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
if ($hash) {
$text .= '<!-- File hash: ' . $hash . ' -->';
}
return $text;
}
public function __toString() {
return $this->get_value();
return $this->value;
}
/**
+122 -42
View File
@@ -1347,6 +1347,12 @@ class question_attempt {
*/
const PARAM_FILES = 'paramfiles';
/**
* @var string special value to indicate a response variable that is uploaded
* files.
*/
const PARAM_CLEANHTML_FILES = 'paramcleanhtmlfiles';
/** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
protected $id = null;
@@ -1661,6 +1667,21 @@ class question_attempt {
return $default;
}
/**
* Get the last step with a particular question type varialbe set.
* @param string $name the name of the variable to get.
* @return question_attempt_step the last step, or a step with no variables
* if there was not a real step.
*/
public function get_last_step_with_qt_var($name) {
foreach ($this->get_reverse_step_iterator() as $step) {
if ($step->has_qt_var($name)) {
return $step;
}
}
return new question_attempt_step_read_only();
}
/**
* Get the latest value of a particular question type variable. That is, get
* the value from the latest step that has it set. Return null if it is not
@@ -1672,12 +1693,12 @@ class question_attempt {
* @return mixed string value, or $default if it has never been set.
*/
public function get_last_qt_var($name, $default = null) {
foreach ($this->get_reverse_step_iterator() as $step) {
if ($step->has_qt_var($name)) {
return $step->get_qt_var($name);
}
$step = $this->get_last_step_with_qt_var($name);
if ($step->has_qt_var($name)) {
return $step->get_qt_var($name);
} else {
return $default;
}
return $default;
}
/**
@@ -1890,6 +1911,15 @@ class question_attempt {
return $this->behaviour->summarise_action($step);
}
/**
* Helper function used by {@link rewrite_pluginfile_urls()} and
* {@link rewrite_response_pluginfile_urls()}.
* @return array ids that need to go into the file paths.
*/
protected function extra_file_path_components() {
return array($this->get_usage_id(), $this->get_slot());
}
/**
* Calls {@link question_rewrite_question_urls()} with appropriate parameters
* for content belonging to this question.
@@ -1897,11 +1927,28 @@ class question_attempt {
* @param string $component the component name (normally 'question' or 'qtype_...')
* @param string $filearea the name of the file area.
* @param int $itemid the item id.
* @return srting the content with the URLs rewritten.
*/
public function rewrite_pluginfile_urls($text, $component, $filearea, $itemid) {
return question_rewrite_question_urls($text,
'pluginfile.php', $this->question->contextid, $component, $filearea,
array($this->get_usage_id(), $this->get_slot()), $itemid);
return question_rewrite_question_urls($text, 'pluginfile.php',
$this->question->contextid, $component, $filearea,
$this->extra_file_path_components(), $itemid);
}
/**
* Calls {@link question_rewrite_question_urls()} with appropriate parameters
* for content belonging to responses to this question.
*
* @param string $text the text to update the URLs in.
* @param int $contextid the id of the context the quba belongs to.
* @param string $name the variable name the files belong to.
* @param question_attempt_step $step the step the response is coming from.
* @return srting the content with the URLs rewritten.
*/
public function rewrite_response_pluginfile_urls($text, $contextid, $name,
question_attempt_step $step) {
return $step->rewrite_response_pluginfile_urls($text, $contextid, $name,
$this->extra_file_path_components());
}
/**
@@ -2043,61 +2090,66 @@ class question_attempt {
* {@link optional_param()}, except that the results is returned without
* slashes.
* @param string $name the paramter name.
* @param int $type one of the PARAM_... constants.
* @param int $type one of the standard PARAM_... constants, or one of the
* special extra constands defined by this class.
* @param array $postdata (optional, only inteded for testing use) take the
* data from this array, instead of from $_POST.
* @return mixed the requested value.
*/
public function get_submitted_var($name, $type, $postdata = null) {
// Special case to work around PARAM_NUMBER converting '' to 0.
if ($type == self::PARAM_MARK) {
$mark = $this->get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata);
if ($mark === '') {
return $mark;
} else {
return $this->get_submitted_var($name, PARAM_NUMBER, $postdata);
}
}
switch ($type) {
case self::PARAM_MARK:
// Special case to work around PARAM_NUMBER converting '' to 0.
$mark = $this->get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata);
if ($mark === '') {
return $mark;
} else {
return $this->get_submitted_var($name, PARAM_NUMBER, $postdata);
}
if ($type == self::PARAM_FILES) {
return $this->process_response_files($name, $postdata);
}
case self::PARAM_FILES:
return $this->process_response_files($name, $name, $postdata);
if (is_null($postdata)) {
$var = optional_param($name, null, $type);
} else if (array_key_exists($name, $postdata)) {
$var = clean_param($postdata[$name], $type);
} else {
$var = null;
}
case self::PARAM_CLEANHTML_FILES:
$var = $this->get_submitted_var($name, PARAM_CLEANHTML, $postdata);
return $this->process_response_files($name, $name . ':itemid', $postdata, $var);
if (is_string($var)) {
$var = $var;
}
default:
if (is_null($postdata)) {
$var = optional_param($name, null, $type);
} else if (array_key_exists($name, $postdata)) {
$var = clean_param($postdata[$name], $type);
} else {
$var = null;
}
return $var;
return $var;
}
}
/**
* Handle a submitted variable representing uploaded files.
* @param string $name the field name.
* @param string $draftidname the field name holding the draft file area id.
* @param array $postdata (optional, only inteded for testing use) take the
* data from this array, instead of from $_POST. At the moment, this
* behaves as if there were no files.
* @param string $text optional reponse text.
* @return question_file_saver that can be used to save the files later.
*/
protected function process_response_files($name, $postdata = null) {
protected function process_response_files($name, $draftidname, $postdata = null, $text = null) {
if ($postdata) {
// There can be no files with test data (at the moment).
return null;
}
$draftitemid = file_get_submitted_draft_itemid($name);
$draftitemid = file_get_submitted_draft_itemid($draftidname);
if (!$draftitemid) {
return null;
}
return new question_file_saver($draftitemid, 'question', 'response_' .
str_replace($this->get_field_prefix(), '', $name));
str_replace($this->get_field_prefix(), '', $name), $text);
}
/**
@@ -2612,10 +2664,6 @@ class question_attempt_step {
}
}
public function get_id() {
return $this->id; // TODO get rid of this.
}
/** @return question_state The state after this step. */
public function get_state() {
return $this->state;
@@ -2719,12 +2767,44 @@ class question_attempt_step {
* @return int the draft itemid.
*/
public function prepare_response_files_draft_itemid($name, $contextid) {
$draftid = 0; // Will be filled in by file_prepare_draft_area.
file_prepare_draft_area($draftid, $contextid, 'question',
'response_' . $name, $this->id);
list($draftid, $notused) = $this->prepare_response_files_draft_itemid_with_text(
$name, $contextid, null);
return $draftid;
}
/**
* Prepare a draft file are for the files belonging the a response variable
* of this step, while rewriting the URLs in some text.
*
* @param string $name the variable name the files belong to.
* @param int $contextid the id of the context the quba belongs to.
* @param string $text the text to update the URLs in.
* @return array(int, string) the draft itemid and the text with URLs rewritten.
*/
public function prepare_response_files_draft_itemid_with_text($name, $contextid, $text) {
$draftid = 0; // Will be filled in by file_prepare_draft_area.
$newtext = file_prepare_draft_area($draftid, $contextid, 'question',
'response_' . $name, $this->id, null, $text);
return array($draftid, $newtext);
}
/**
* Rewrite the @@PLUGINFILE@@ tokens in a response variable from this step
* that contains links to file. Normally you should probably call
* {@link question_attempt::rewrite_response_pluginfile_urls()} instead of
* calling this method directly.
*
* @param string $text the text to update the URLs in.
* @param int $contextid the id of the context the quba belongs to.
* @param string $name the variable name the files belong to.
* @param array $extra extra file path components.
* @return string the rewritten text.
*/
public function rewrite_response_pluginfile_urls($text, $contextid, $name, $extras) {
return question_rewrite_question_urls($text, 'pluginfile.php', $contextid,
'question', 'response_' . $name, $extras, $this->id);
}
/**
* Get all the question type variables.
* @param array name => value pairs.
+10 -1
View File
@@ -55,7 +55,12 @@ class qtype_essay_question extends question_with_responses {
}
public function get_expected_data() {
$expecteddata = array('answer' => PARAM_CLEANHTML, 'answerformat' => PARAM_FORMAT);
if ($this->responseformat == 'editorfilepicker') {
$expecteddata = array('answer' => question_attempt::PARAM_CLEANHTML_FILES);
} else {
$expecteddata = array('answer' => PARAM_CLEANHTML);
}
$expecteddata['answerformat'] = PARAM_FORMAT;
if ($this->attachments != 0) {
$expecteddata['attachments'] = question_attempt::PARAM_FILES;
}
@@ -92,6 +97,10 @@ class qtype_essay_question extends question_with_responses {
// Response attachments visible if the question has them.
return $this->attachments != 0;
} else if ($component == 'question' && $filearea == 'response_answer') {
// Response attachments visible if the question has them.
return $this->responseformat === 'editorfilepicker';
} else if ($component == 'qtype_essay' && $filearea == 'graderinfo') {
return $options->manualcomment;
+171 -47
View File
@@ -42,17 +42,14 @@ class qtype_essay_renderer extends qtype_renderer {
$responseoutput = $question->get_format_renderer($this->page);
// Answer field.
$inputname = $qa->get_qt_field_name('answer');
$response = $qa->get_last_qt_var('answer', '');
$responseformat = $qa->get_last_qt_var('answerformat', FORMAT_HTML);
$step = $qa->get_last_step_with_qt_var('answer');
if (empty($options->readonly)) {
$answer = $responseoutput->response_area_input($inputname,
$response, $responseformat, $question->responsefieldlines,
$options->context);
$answer = $responseoutput->response_area_input('answer', $qa,
$step, $question->responsefieldlines, $options->context);
} else {
$answer = $responseoutput->response_area_read_only($inputname,
$response, $responseformat, $question->responsefieldlines);
$answer = $responseoutput->response_area_read_only('answer', $qa,
$step, $question->responsefieldlines, $options->context);
}
$files = '';
@@ -114,6 +111,9 @@ class qtype_essay_renderer extends qtype_renderer {
'attachments', $options->context->id);
$pickeroptions->context = $options->context;
$pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
'attachments', $options->context->id);
return form_filemanager_render($pickeroptions) . html_writer::empty_tag(
'input', array('type' => 'hidden', 'name' => $qa->get_qt_field_name('attachments'),
'value' => $pickeroptions->itemid));
@@ -142,21 +142,32 @@ class qtype_essay_renderer extends qtype_renderer {
abstract class qtype_essay_format_renderer_base extends plugin_renderer_base {
/**
* Render the students respone when the question is in read-only mode.
* @param string $inputname the field name to use for this input.
* @param string $response the student's current response.
* @param string $name the variable name this input edits.
* @param question_attempt $qa the question attempt being display.
* @param question_attempt_step $step the current step.
* @param int $lines approximate size of input box to display.
* @param object $context the context teh output belongs to.
* @return string html to display the response.
*/
public abstract function response_area_read_only($inputname, $response,
$responseformat, $lines);
public abstract function response_area_read_only($name, question_attempt $qa,
question_attempt_step $step, $lines, $context);
/**
* Render the students respone when the question is in read-only mode.
* @param string $inputname the field name to use for this input.
* @param string $response the student's current response.
* @param string $name the variable name this input edits.
* @param question_attempt $qa the question attempt being display.
* @param question_attempt_step $step the current step.
* @param int $lines approximate size of input box to display.
* @param object $context the context teh output belongs to.
* @return string html to display the response for editing.
*/
public abstract function response_area_input($inputname, $response,
$responseformat, $lines, $contex);
public abstract function response_area_input($name, question_attempt $qa,
question_attempt_step $step, $lines, $context);
/**
* @return string specific class name to add to the input element.
*/
protected abstract function class_name();
}
@@ -168,18 +179,21 @@ abstract class qtype_essay_format_renderer_base extends plugin_renderer_base {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_format_editor_renderer extends plugin_renderer_base {
public function response_area_read_only($inputname, $response, $responseformat, $lines) {
$formatoptions = new stdClass();
$formatoptions->para = false;
$response = $this->rewrite_pluginfile_urls($response);
return html_writer::tag('div', format_text($response, $responseformat, $formatoptions),
array('class' => 'qtype_essay_editor qtype_essay_response'));
protected function class_name() {
return 'qtype_essay_editor';
}
public function response_area_input($inputname, $response, $responseformat, $lines, $context) {
public function response_area_read_only($name, $qa, $step, $lines, $context) {
return html_writer::tag('div', $this->prepare_response($name, $qa, $step, $context),
array('class' => $this->class_name() . ' qtype_essay_response'));
}
public function response_area_input($name, $qa, $step, $lines, $context) {
global $CFG, $PAGE;
require_once($CFG->dirroot.'/repository/lib.php');
$inputname = $qa->get_qt_field_name($name);
$responseformat = $step->get_qt_var($name . 'format');
$id = $inputname . '_id';
$editor = editors_get_preferred_editor($responseformat);
@@ -189,13 +203,17 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
$formats[$fid] = $strformats[$fid];
}
list($draftitemid, $reponse) = $this->prepare_response_for_editing(
$name, $step, $context);
$editor->use_editor($id, $this->get_editor_options($context),
$this->get_filepicker_options());
$this->get_filepicker_options($context, $draftitemid));
$output = '';
$output .= html_writer::start_tag('div');
$output .= html_writer::start_tag('div', array('class' =>
$this->class_name() . ' qtype_essay_response'));
$output .= html_writer::tag('div', html_writer::tag('textarea', s($response),
$output .= html_writer::tag('div', html_writer::tag('textarea', s($reponse),
array('id' => $id, 'name' => $inputname, 'rows' => $lines)));
$output .= html_writer::start_tag('div');
@@ -209,28 +227,46 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
}
$output .= html_writer::end_tag('div');
$output .= $this->nonjs_filepicker();
$output .= $this->filepicker_html($inputname, $draftitemid);
$output .= html_writer::end_tag('div');
return $output;
}
/**
* @param string $response the student's response.
* @return string the response with file URLs processed.
* Prepare the response for read-only display.
* @param string $name the variable name this input edits.
* @param question_attempt $qa the question attempt being display.
* @param question_attempt_step $step the current step.
* @param object $context the context the attempt belongs to.
* @return string the response prepared for display.
*/
protected function rewrite_pluginfile_urls($response) {
return $response;
protected function prepare_response($name, question_attempt $qa,
question_attempt_step $step, $context) {
if (!$step->has_qt_var($name)) {
return '';
}
$formatoptions = new stdClass();
$formatoptions->para = false;
return format_text($step->get_qt_var($name), $step->get_qt_var($name . 'format'),
$formatoptions);
}
/**
* @return array filepicker options for the editor.
* Prepare the response for editing.
* @param string $name the variable name this input edits.
* @param question_attempt_step $step the current step.
* @param object $context the context the attempt belongs to.
* @return string the response prepared for display.
*/
protected function get_filepicker_options() {
return array();
protected function prepare_response_for_editing($name,
question_attempt_step $step, $context) {
return array(0, $step->get_qt_var($name));
}
/**
* @param object $context the context the attempt belongs to.
* @return array options for the editor.
*/
protected function get_editor_options($context) {
@@ -238,9 +274,20 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
}
/**
* Extra output for the filepicker, if used.
* @param object $context the context the attempt belongs to.
* @param int $draftitemid draft item id.
* @return array filepicker options for the editor.
*/
protected function nonjs_filepicker() {
protected function get_filepicker_options($context, $draftitemid) {
return array();
}
/**
* @param string $inputname input field name.
* @param int $draftitemid draft file area itemid.
* @return string HTML for the filepicker, if used.
*/
protected function filepicker_html($inputname, $draftitemid) {
return '';
}
}
@@ -254,17 +301,93 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_format_editorfilepicker_renderer extends qtype_essay_format_editor_renderer {
protected function rewrite_pluginfile_urls($response) {
return $response;
protected function class_name() {
return 'qtype_essay_editorfilepicker';
}
protected function get_filepicker_options() {
return array();
protected function prepare_response($name, question_attempt $qa,
question_attempt_step $step, $context) {
if (!$step->has_qt_var($name)) {
return '';
}
$formatoptions = new stdClass();
$formatoptions->para = false;
$text = $qa->rewrite_response_pluginfile_urls($step->get_qt_var($name),
$context->id, 'answer', $step);
return format_text($text, $step->get_qt_var($name . 'format'), $formatoptions);
}
protected function nonjs_filepicker() {
return '';
protected function prepare_response_for_editing($name,
question_attempt_step $step, $context) {
return $step->prepare_response_files_draft_itemid_with_text(
$name, $context->id, $step->get_qt_var($name));
}
protected function get_editor_options($context) {
return array(
'subdirs' => 0,
'maxbytes' => 0,
'maxfiles' => -1,
'context' => $context,
'noclean' => 0,
'trusttext'=>0
);
}
/**
* Get the options required to configure the filepicker for one of the editor
* toolbar buttons.
* @param mixed $acceptedtypes array of types of '*'.
* @param int $draftitemid the draft area item id.
* @param object $context the context.
* @return object the required options.
*/
protected function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
$filepickeroptions = new stdClass();
$filepickeroptions->accepted_types = $acceptedtypes;
$filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
$filepickeroptions->context = $context;
$filepickeroptions->env = 'filepicker';
$options = initialise_filepicker($filepickeroptions);
$options->context = $context;
$options->client_id = uniqid();
$options->env = 'editor';
$options->itemid = $draftitemid;
return $options;
}
protected function get_filepicker_options($context, $draftitemid) {
global $CFG;
return array(
'image' => $this->specific_filepicker_options(array('image'),
$draftitemid, $context),
'media' => $this->specific_filepicker_options(array('video', 'audio'),
$draftitemid, $context),
'link' => $this->specific_filepicker_options('*',
$draftitemid, $context),
);
}
protected function filepicker_html($inputname, $draftitemid) {
$nonjspickerurl = new moodle_url('/repository/draftfiles_manager.php', array(
'action' => 'browse',
'env' => 'editor',
'itemid' => $draftitemid,
'subdirs' => false,
'maxfiles' => -1,
'sesskey' => sesskey(),
));
return html_writer::empty_tag('input', array('type' => 'hidden',
'name' => $inputname . ':itemid', 'value' => $draftitemid)) .
html_writer::tag('noscript', html_writer::tag('div',
html_writer::tag('object', '', array('type' => 'text/html',
'data' => $nonjspickerurl, 'height' => 160, 'width' => 600,
'style' => 'border: 1px solid #000;'))));
}
}
@@ -290,12 +413,13 @@ class qtype_essay_format_plain_renderer extends plugin_renderer_base {
return 'qtype_essay_plain';
}
public function response_area_read_only($inputname, $response, $responseformat, $lines) {
return $this->textarea($response, $lines, array('readonly' => 'readonly'));
public function response_area_read_only($name, $qa, $step, $lines, $context) {
return $this->textarea($step->get_qt_var($name), $lines, array('readonly' => 'readonly'));
}
public function response_area_input($inputname, $response, $responseformat, $lines, $contex) {
return $this->textarea($response, $lines, array('name' => $inputname)) .
public function response_area_input($name, $qa, $step, $lines, $context) {
$inputname = $qa->get_qt_field_name($name);
return $this->textarea($step->get_qt_var($name), $lines, array('name' => $inputname)) .
html_writer::empty_tag('input', array('type' => 'hidden',
'name' => $inputname . 'format', 'value' => FORMAT_PLAIN));
}
+4 -1
View File
@@ -9,7 +9,10 @@
white-space: pre;
font-family: Andale Mono, Monaco, Courier New, DejaVu Sans Mono, monospace;
}
.que.essay .qtype_essay_response.qtype_essay_editor {
.que.essay .qtype_essay_response {
background-color: white;
min-height: 3em;
}
.que.essay div.qtype_essay_response textarea {
width: 100%;
}