Merge branch 'MDL-47494' of git://github.com/timhunt/moodle
Note: Fixed some minor grunt and EOF whitespace issues while merging.
@@ -1111,7 +1111,8 @@ class core_plugin_manager {
|
||||
|
||||
'qtype' => array(
|
||||
'calculated', 'calculatedmulti', 'calculatedsimple',
|
||||
'description', 'essay', 'match', 'missingtype', 'multianswer',
|
||||
'ddimageortext', 'ddmarker', 'ddwtos', 'description',
|
||||
'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
|
||||
'multichoice', 'numerical', 'random', 'randomsamatch',
|
||||
'shortanswer', 'truefalse'
|
||||
),
|
||||
|
||||
@@ -411,6 +411,15 @@ class behat_general extends behat_base {
|
||||
list($containerselector, $containerlocator) = $this->transform_selector($containerselectortype, $containerelement);
|
||||
$destinationxpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($containerselector, $containerlocator);
|
||||
|
||||
$node = $this->get_selected_node("xpath_element", $sourcexpath);
|
||||
if (!$node->isVisible()) {
|
||||
throw new ExpectationException('"' . $sourcexpath . '" "xpath_element" is not visible', $this->getSession());
|
||||
}
|
||||
$node = $this->get_selected_node("xpath_element", $destinationxpath);
|
||||
if (!$node->isVisible()) {
|
||||
throw new ExpectationException('"' . $destinationxpath . '" "xpath_element" is not visible', $this->getSession());
|
||||
}
|
||||
|
||||
$this->getSession()->getDriver()->dragTo($sourcexpath, $destinationxpath);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Backup code for qtype_ddimageortext.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Provides the information to backup ddimageortext questions.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backup_qtype_ddimageortext_plugin extends backup_qtype_plugin {
|
||||
/**
|
||||
* Returns the question type this is.
|
||||
*
|
||||
* @return string question type name, like 'ddimageortext'.
|
||||
*/
|
||||
protected static function qtype_name() {
|
||||
return 'ddimageortext';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the qtype information to attach to question element.
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
$qtype = self::qtype_name();
|
||||
$plugin = $this->get_plugin_element(null, '../../qtype', $qtype);
|
||||
|
||||
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
|
||||
|
||||
$plugin->add_child($pluginwrapper);
|
||||
|
||||
$dds = new backup_nested_element($qtype, array('id'), array(
|
||||
'shuffleanswers', 'correctfeedback', 'correctfeedbackformat',
|
||||
'partiallycorrectfeedback', 'partiallycorrectfeedbackformat',
|
||||
'incorrectfeedback', 'incorrectfeedbackformat', 'shownumcorrect'));
|
||||
|
||||
$pluginwrapper->add_child($dds);
|
||||
$drags = new backup_nested_element('drags');
|
||||
|
||||
$drag = new backup_nested_element('drag', array('id'),
|
||||
array('no', 'draggroup', 'infinite', 'label'));
|
||||
$drops = new backup_nested_element('drops');
|
||||
|
||||
$drop = new backup_nested_element('drop', array('id'),
|
||||
array('no', 'xleft', 'ytop', 'choice', 'label'));
|
||||
|
||||
$dds->set_source_table("qtype_{$qtype}",
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
$pluginwrapper->add_child($drags);
|
||||
$drags->add_child($drag);
|
||||
$pluginwrapper->add_child($drops);
|
||||
$drops->add_child($drop);
|
||||
|
||||
$drag->set_source_table("qtype_{$qtype}_drags",
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
$drop->set_source_table("qtype_{$qtype}_drops",
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one array with filearea => mappingname elements for the qtype
|
||||
*
|
||||
* Used by {@link get_components_and_fileareas} to know about all the qtype
|
||||
* files to be processed both in backup and restore.
|
||||
*/
|
||||
public static function get_qtype_fileareas() {
|
||||
$qtype = self::qtype_name();
|
||||
return array(
|
||||
'correctfeedback' => 'question_created',
|
||||
'partiallycorrectfeedback' => 'question_created',
|
||||
'incorrectfeedback' => 'question_created',
|
||||
|
||||
'bgimage' => 'question_created',
|
||||
'dragimage' => "qtype_{$qtype}_drags");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Restore code for qtype_ddimageortext.
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Restore plugin class for the ddimageortext question type.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_qtype_ddimageortext_plugin extends restore_qtype_plugin {
|
||||
/**
|
||||
* Returns the qtype name.
|
||||
*
|
||||
* @return string The type name
|
||||
*/
|
||||
protected static function qtype_name() {
|
||||
return 'ddimageortext';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths to be handled by the plugin at question level.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
|
||||
$paths = array();
|
||||
|
||||
// Add own qtype stuff.
|
||||
$elename = 'dds';
|
||||
$elepath = $this->get_pathfor('/'.self::qtype_name());
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
$elename = 'drag';
|
||||
$elepath = $this->get_pathfor('/drags/drag');
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
$elename = 'drop';
|
||||
$elepath = $this->get_pathfor('/drops/drop');
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
return $paths; // And we return the interesting paths.
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/{qtypename} element.
|
||||
*
|
||||
* @param array|object $data Drag and drop data to work with.
|
||||
*/
|
||||
public function process_dds($data) {
|
||||
global $DB;
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
// If the question has been created by restore,
|
||||
// we need to create its qtype_ddimageortext too.
|
||||
if ($questioncreated) {
|
||||
// Adjust some columns.
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record($prefix, $data);
|
||||
// Create mapping (needed for decoding links).
|
||||
$this->set_mapping($prefix, $oldid, $newitemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/drags/drag element.
|
||||
*
|
||||
* @param array|object $data Drag and drop drag data to work with.
|
||||
*/
|
||||
public function process_drag($data) {
|
||||
global $DB;
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
if ($questioncreated) {
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record("{$prefix}_drags", $data);
|
||||
// Create mapping (there are files and states based on this).
|
||||
$this->set_mapping("{$prefix}_drags", $oldid, $newitemid);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/drags/drop element.
|
||||
*
|
||||
* @param array|object $data Drad and drop drops data to work with.
|
||||
*/
|
||||
public function process_drop($data) {
|
||||
global $DB;
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
if ($questioncreated) {
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record("{$prefix}_drops", $data);
|
||||
// Create mapping (there are files and states based on this).
|
||||
$this->set_mapping("{$prefix}_drops", $oldid, $newitemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this qtype to be processed by the links decoder.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function define_decode_contents() {
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$contents = array();
|
||||
|
||||
$fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
|
||||
$contents[] =
|
||||
new restore_decode_content($prefix, $fields, $prefix);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="question/type/ddimageortext/db" VERSION="20150914" COMMENT="XMLDB file for Moodle question/type/ddimageortext."
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
<TABLES>
|
||||
<TABLE NAME="qtype_ddimageortext" COMMENT="Defines drag and drop (text or images onto a background image) questions">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
|
||||
<FIELD NAME="correctfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any correct response."/>
|
||||
<FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="partiallycorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any partially correct response."/>
|
||||
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="incorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response."/>
|
||||
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="qtype_ddimageortext_drops" COMMENT="Drop boxes">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="no" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="drop number"/>
|
||||
<FIELD NAME="xleft" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="ytop" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="choice" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="label" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Alt label for drop box"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="qtype_ddimageortext_drags" COMMENT="Images to drag. Actual file names are not stored here we use the file names as found in the file storage area.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="no" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="drag no"/>
|
||||
<FIELD NAME="draggroup" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="infinite" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="label" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Alt text label for drag-able image."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
@@ -0,0 +1,286 @@
|
||||
<?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 the editing form for the drag-and-drop images onto images question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/edit_ddtoimage_form_base.php');
|
||||
|
||||
|
||||
/**
|
||||
* Drag-and-drop images onto images editing form definition.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_edit_form extends qtype_ddtoimage_edit_form_base {
|
||||
public function qtype() {
|
||||
return 'ddimageortext';
|
||||
}
|
||||
|
||||
public function data_preprocessing($question) {
|
||||
$question = parent::data_preprocessing($question);
|
||||
$question = $this->data_preprocessing_combined_feedback($question, true);
|
||||
$question = $this->data_preprocessing_hints($question, true, true);
|
||||
|
||||
$dragids = array(); // Drag no -> dragid.
|
||||
if (!empty($question->options)) {
|
||||
$question->shuffleanswers = $question->options->shuffleanswers;
|
||||
$question->drags = array();
|
||||
foreach ($question->options->drags as $drag) {
|
||||
$dragindex = $drag->no - 1;
|
||||
$question->drags[$dragindex] = array();
|
||||
$question->draglabel[$dragindex] = $drag->label;
|
||||
$question->drags[$dragindex]['infinite'] = $drag->infinite;
|
||||
$question->drags[$dragindex]['draggroup'] = $drag->draggroup;
|
||||
$dragids[$dragindex] = $drag->id;
|
||||
}
|
||||
$question->drops = array();
|
||||
foreach ($question->options->drops as $drop) {
|
||||
$question->drops[$drop->no - 1] = array();
|
||||
$question->drops[$drop->no - 1]['choice'] = $drop->choice;
|
||||
$question->drops[$drop->no - 1]['droplabel'] = $drop->label;
|
||||
$question->drops[$drop->no - 1]['xleft'] = $drop->xleft;
|
||||
$question->drops[$drop->no - 1]['ytop'] = $drop->ytop;
|
||||
}
|
||||
}
|
||||
// Initialise file picker for bgimage.
|
||||
$draftitemid = file_get_submitted_draft_itemid('bgimage');
|
||||
|
||||
file_prepare_draft_area($draftitemid, $this->context->id, 'qtype_ddimageortext',
|
||||
'bgimage', !empty($question->id) ? (int) $question->id : null,
|
||||
self::file_picker_options());
|
||||
$question->bgimage = $draftitemid;
|
||||
|
||||
// Initialise file picker for dragimages.
|
||||
list(, $imagerepeats) = $this->get_drag_item_repeats();
|
||||
$draftitemids = optional_param_array('dragitem', array(), PARAM_INT);
|
||||
for ($imageindex = 0; $imageindex < $imagerepeats; $imageindex++) {
|
||||
$draftitemid = isset($draftitemids[$imageindex]) ? $draftitemids[$imageindex] : 0;
|
||||
// Numbers not allowed in filearea name.
|
||||
$itemid = isset($dragids[$imageindex]) ? $dragids[$imageindex] : null;
|
||||
file_prepare_draft_area($draftitemid, $this->context->id, 'qtype_ddimageortext',
|
||||
'dragimage', $itemid, self::file_picker_options());
|
||||
$question->dragitem[$imageindex] = $draftitemid;
|
||||
}
|
||||
if (!empty($question->options)) {
|
||||
foreach ($question->options->drags as $drag) {
|
||||
$dragindex = $drag->no - 1;
|
||||
if (!isset($question->dragitem[$dragindex])) {
|
||||
$fileexists = false;
|
||||
} else {
|
||||
$fileexists = self::file_uploaded($question->dragitem[$dragindex]);
|
||||
}
|
||||
$labelexists = (trim($question->draglabel[$dragindex]) != '');
|
||||
if ($labelexists && !$fileexists) {
|
||||
$question->drags[$dragindex]['dragitemtype'] = 'word';
|
||||
} else {
|
||||
$question->drags[$dragindex]['dragitemtype'] = 'image';
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->js_call();
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
|
||||
public function js_call() {
|
||||
global $PAGE;
|
||||
$maxsizes = new stdClass();
|
||||
$maxsizes->bgimage = new stdClass();
|
||||
$maxsizes->bgimage->width = QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH;
|
||||
$maxsizes->bgimage->height = QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT;
|
||||
$maxsizes->dragimage = new stdClass();
|
||||
$maxsizes->dragimage->width = QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH;
|
||||
$maxsizes->dragimage->height = QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT;
|
||||
|
||||
$params = array('maxsizes' => $maxsizes,
|
||||
'topnode' => 'fieldset#id_previewareaheader');
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddimageortext-form',
|
||||
'M.qtype_ddimageortext.init_form',
|
||||
array($params));
|
||||
}
|
||||
|
||||
// Drag items.
|
||||
|
||||
protected function definition_draggable_items($mform, $itemrepeatsatstart) {
|
||||
$mform->addElement('header', 'draggableitemheader',
|
||||
get_string('draggableitems', 'qtype_ddimageortext'));
|
||||
$mform->addElement('advcheckbox', 'shuffleanswers', ' ',
|
||||
get_string('shuffleimages', 'qtype_'.$this->qtype()));
|
||||
$mform->setDefault('shuffleanswers', 0);
|
||||
$this->repeat_elements($this->draggable_item($mform), $itemrepeatsatstart,
|
||||
$this->draggable_items_repeated_options(),
|
||||
'noitems', 'additems', self::ADD_NUM_ITEMS,
|
||||
get_string('addmoreimages', 'qtype_ddimageortext'), true);
|
||||
}
|
||||
|
||||
protected function draggable_item($mform) {
|
||||
$draggableimageitem = array();
|
||||
|
||||
$grouparray = array();
|
||||
$dragitemtypes = array('image' => get_string('draggableimage', 'qtype_ddimageortext'),
|
||||
'word' => get_string('draggableword', 'qtype_ddimageortext'));
|
||||
$grouparray[] = $mform->createElement('select', 'dragitemtype',
|
||||
get_string('draggableitemtype', 'qtype_ddimageortext'),
|
||||
$dragitemtypes,
|
||||
array('class' => 'dragitemtype'));
|
||||
$options = array();
|
||||
for ($i = 1; $i <= self::MAX_GROUPS; $i += 1) {
|
||||
$options[$i] = $i;
|
||||
}
|
||||
$grouparray[] = $mform->createElement('select', 'draggroup',
|
||||
get_string('group', 'qtype_gapselect'),
|
||||
$options,
|
||||
array('class' => 'draggroup'));
|
||||
$grouparray[] = $mform->createElement('advcheckbox', 'infinite', ' ',
|
||||
get_string('infinite', 'qtype_ddimageortext'));
|
||||
$draggableimageitem[] = $mform->createElement('group', 'drags',
|
||||
get_string('draggableitemheader', 'qtype_ddimageortext', '{no}'), $grouparray);
|
||||
|
||||
$draggableimageitem[] = $mform->createElement('filepicker', 'dragitem', '', null,
|
||||
self::file_picker_options());
|
||||
|
||||
$draggableimageitem[] = $mform->createElement('text', 'draglabel',
|
||||
get_string('label', 'qtype_ddimageortext'),
|
||||
array('size' => 30, 'class' => 'tweakcss'));
|
||||
$mform->setType('draglabel', PARAM_RAW); // These are validated manually.
|
||||
return $draggableimageitem;
|
||||
}
|
||||
|
||||
protected function draggable_items_repeated_options() {
|
||||
$repeatedoptions = array();
|
||||
$repeatedoptions['draggroup']['default'] = '1';
|
||||
return $repeatedoptions;
|
||||
}
|
||||
|
||||
// Drop zones.
|
||||
|
||||
protected function drop_zone($mform, $imagerepeats) {
|
||||
$dropzoneitem = array();
|
||||
|
||||
$grouparray = array();
|
||||
$grouparray[] = $mform->createElement('text', 'xleft',
|
||||
get_string('xleft', 'qtype_ddimageortext'),
|
||||
array('size' => 5, 'class' => 'tweakcss'));
|
||||
$grouparray[] = $mform->createElement('text', 'ytop',
|
||||
get_string('ytop', 'qtype_ddimageortext'),
|
||||
array('size' => 5, 'class' => 'tweakcss'));
|
||||
$options = array();
|
||||
|
||||
$options[0] = '';
|
||||
for ($i = 1; $i <= $imagerepeats; $i += 1) {
|
||||
$options[$i] = $i;
|
||||
}
|
||||
$grouparray[] = $mform->createElement('select', 'choice',
|
||||
get_string('draggableitem', 'qtype_ddimageortext'), $options);
|
||||
$grouparray[] = $mform->createElement('text', 'droplabel',
|
||||
get_string('label', 'qtype_ddimageortext'),
|
||||
array('size' => 10, 'class' => 'tweakcss'));
|
||||
$mform->setType('droplabel', PARAM_NOTAGS);
|
||||
$dropzone = $mform->createElement('group', 'drops',
|
||||
get_string('dropzone', 'qtype_ddimageortext', '{no}'), $grouparray);
|
||||
return array($dropzone);
|
||||
}
|
||||
|
||||
protected function drop_zones_repeated_options() {
|
||||
$repeatedoptions = array();
|
||||
// The next two are PARAM_RAW becuase we need to distinguish 0 and ''.
|
||||
// We do the necessary validation in the validation method.
|
||||
$repeatedoptions['drops[xleft]']['type'] = PARAM_RAW;
|
||||
$repeatedoptions['drops[ytop]']['type'] = PARAM_RAW;
|
||||
$repeatedoptions['drops[droplabel]']['type'] = PARAM_RAW;
|
||||
$repeatedoptions['choice']['default'] = '0';
|
||||
return $repeatedoptions;
|
||||
}
|
||||
|
||||
public function validation($data, $files) {
|
||||
$errors = parent::validation($data, $files);
|
||||
if (!self::file_uploaded($data['bgimage'])) {
|
||||
$errors["bgimage"] = get_string('formerror_nobgimage', 'qtype_'.$this->qtype());
|
||||
}
|
||||
|
||||
$allchoices = array();
|
||||
for ($i = 0; $i < $data['nodropzone']; $i++) {
|
||||
$ytoppresent = (trim($data['drops'][$i]['ytop']) !== '');
|
||||
$xleftpresent = (trim($data['drops'][$i]['xleft']) !== '');
|
||||
$ytopisint = (string) clean_param($data['drops'][$i]['ytop'], PARAM_INT) === trim($data['drops'][$i]['ytop']);
|
||||
$xleftisint = (string) clean_param($data['drops'][$i]['xleft'], PARAM_INT) === trim($data['drops'][$i]['xleft']);
|
||||
$labelpresent = (trim($data['drops'][$i]['droplabel']) !== '');
|
||||
$choice = $data['drops'][$i]['choice'];
|
||||
$imagechoicepresent = ($choice !== '0');
|
||||
|
||||
if ($imagechoicepresent) {
|
||||
if (!$ytoppresent) {
|
||||
$errors["drops[$i]"] = get_string('formerror_noytop', 'qtype_ddimageortext');
|
||||
} else if (!$ytopisint) {
|
||||
$errors["drops[$i]"] = get_string('formerror_notintytop', 'qtype_ddimageortext');
|
||||
}
|
||||
if (!$xleftpresent) {
|
||||
$errors["drops[$i]"] = get_string('formerror_noxleft', 'qtype_ddimageortext');
|
||||
} else if (!$xleftisint) {
|
||||
$errors["drops[$i]"] = get_string('formerror_notintxleft', 'qtype_ddimageortext');
|
||||
}
|
||||
|
||||
if ($data['drags'][$choice - 1]['dragitemtype'] != 'word' &&
|
||||
!self::file_uploaded($data['dragitem'][$choice - 1])) {
|
||||
$errors['dragitem['.($choice - 1).']'] =
|
||||
get_string('formerror_nofile', 'qtype_ddimageortext', $i);
|
||||
}
|
||||
|
||||
if (isset($allchoices[$choice]) && !$data['drags'][$choice - 1]['infinite']) {
|
||||
$errors["drops[$i]"] =
|
||||
get_string('formerror_multipledraginstance', 'qtype_ddimageortext', $choice);
|
||||
$errors['drops['.($allchoices[$choice]).']'] =
|
||||
get_string('formerror_multipledraginstance', 'qtype_ddimageortext', $choice);
|
||||
$errors['drags['.($choice - 1).']'] =
|
||||
get_string('formerror_multipledraginstance2', 'qtype_ddimageortext', $choice);
|
||||
}
|
||||
$allchoices[$choice] = $i;
|
||||
} else {
|
||||
if ($ytoppresent || $xleftpresent || $labelpresent) {
|
||||
$errors["drops[$i]"] =
|
||||
get_string('formerror_noimageselected', 'qtype_ddimageortext');
|
||||
}
|
||||
}
|
||||
}
|
||||
for ($dragindex = 0; $dragindex < $data['noitems']; $dragindex++) {
|
||||
$label = $data['draglabel'][$dragindex];
|
||||
if ($data['drags'][$dragindex]['dragitemtype'] == 'word') {
|
||||
$allowedtags = '<br><sub><sup><b><i><strong><em>';
|
||||
$errormessage = get_string('formerror_disallowedtags', 'qtype_ddimageortext');
|
||||
} else {
|
||||
$allowedtags = '';
|
||||
$errormessage = get_string('formerror_noallowedtags', 'qtype_ddimageortext');
|
||||
}
|
||||
if ($label != strip_tags($label, $allowedtags)) {
|
||||
$errors["drags[{$dragindex}]"] = $errormessage;
|
||||
}
|
||||
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Base class for editing form for the drag-and-drop images onto images question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Base class for drag-and-drop onto images editing form definition.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class qtype_ddtoimage_edit_form_base extends question_edit_form {
|
||||
/**
|
||||
* Maximum number of different groups of drag items there can be in a question.
|
||||
*/
|
||||
const MAX_GROUPS = 8;
|
||||
|
||||
/**
|
||||
* The default starting number of drop zones.
|
||||
*/
|
||||
const START_NUM_ITEMS = 6;
|
||||
|
||||
/**
|
||||
* The number of drop zones that get added at a time.
|
||||
*/
|
||||
const ADD_NUM_ITEMS = 3;
|
||||
|
||||
/**
|
||||
* Options shared by all file pickers in the form.
|
||||
*
|
||||
* @return array Array of filepicker options.
|
||||
*/
|
||||
public static function file_picker_options() {
|
||||
$filepickeroptions = array();
|
||||
$filepickeroptions['accepted_types'] = array('web_image');
|
||||
$filepickeroptions['maxbytes'] = 0;
|
||||
$filepickeroptions['maxfiles'] = 1;
|
||||
$filepickeroptions['subdirs'] = 0;
|
||||
return $filepickeroptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* definition_inner adds all specific fields to the form.
|
||||
*
|
||||
* @param MoodleQuickForm $mform (the form being built).
|
||||
*/
|
||||
protected function definition_inner($mform) {
|
||||
|
||||
$mform->addElement('header', 'previewareaheader',
|
||||
get_string('previewareaheader', 'qtype_'.$this->qtype()));
|
||||
$mform->setExpanded('previewareaheader');
|
||||
$mform->addElement('static', 'previewarea', '',
|
||||
get_string('previewareamessage', 'qtype_'.$this->qtype()));
|
||||
|
||||
$mform->registerNoSubmitButton('refresh');
|
||||
$mform->addElement('submit', 'refresh', get_string('refresh', 'qtype_'.$this->qtype()));
|
||||
$mform->addElement('filepicker', 'bgimage', get_string('bgimage', 'qtype_'.$this->qtype()),
|
||||
null, self::file_picker_options());
|
||||
$mform->closeHeaderBefore('dropzoneheader');
|
||||
|
||||
// Add the draggable image fields & drop zones to the form.
|
||||
list($itemrepeatsatstart, $imagerepeats) = $this->get_drag_item_repeats();
|
||||
$this->definition_draggable_items($mform, $itemrepeatsatstart);
|
||||
$this->definition_drop_zones($mform, $imagerepeats);
|
||||
|
||||
$this->add_combined_feedback_fields(true);
|
||||
$this->add_interactive_settings(true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make and add drop zones to the form.
|
||||
*
|
||||
* @param object $mform The Moodle form object.
|
||||
* @param int $imagerepeats The initial number of repeat elements.
|
||||
*/
|
||||
protected function definition_drop_zones($mform, $imagerepeats) {
|
||||
$mform->addElement('header', 'dropzoneheader', get_string('dropzoneheader', 'qtype_'.$this->qtype()));
|
||||
|
||||
$countdropzones = 0;
|
||||
if (isset($this->question->id)) {
|
||||
foreach ($this->question->options->drops as $drop) {
|
||||
$countdropzones = max($countdropzones, $drop->no);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$countdropzones) {
|
||||
$countdropzones = self::START_NUM_ITEMS;
|
||||
}
|
||||
$dropzonerepeatsatstart = $countdropzones;
|
||||
|
||||
$this->repeat_elements($this->drop_zone($mform, $imagerepeats), $dropzonerepeatsatstart,
|
||||
$this->drop_zones_repeated_options(),
|
||||
'nodropzone', 'adddropzone', self::ADD_NUM_ITEMS,
|
||||
get_string('addmoredropzones', 'qtype_ddimageortext'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with a drop zone form element.
|
||||
*
|
||||
* @param object $mform The Moodle form object.
|
||||
* @param int $imagerepeats The number of repeat images.
|
||||
* @return array Array with the dropzone element.
|
||||
*/
|
||||
abstract protected function drop_zone($mform, $imagerepeats);
|
||||
|
||||
/**
|
||||
* Returns an array of default drop zone repeat options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function drop_zones_repeated_options();
|
||||
|
||||
/**
|
||||
* Builds and adds the needed form items for draggable items.
|
||||
*
|
||||
* @param object $mform The Moodle form object.
|
||||
* @param int $itemrepeatsatstart The initial number of repeat elements.
|
||||
*/
|
||||
abstract protected function definition_draggable_items($mform, $itemrepeatsatstart);
|
||||
|
||||
/**
|
||||
* Creates and returns a set of form elements to make a draggable item.
|
||||
*
|
||||
* @param object $mform The Moodle form object.
|
||||
* @return array An array of form elements.
|
||||
*/
|
||||
abstract protected function draggable_item($mform);
|
||||
|
||||
/**
|
||||
* Returns an array of default repeat options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function draggable_items_repeated_options();
|
||||
|
||||
/**
|
||||
* Returns an array of starting number of repeats, and the total number of repeats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_drag_item_repeats() {
|
||||
$countimages = 0;
|
||||
if (isset($this->question->id)) {
|
||||
foreach ($this->question->options->drags as $drag) {
|
||||
$countimages = max($countimages, $drag->no);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$countimages) {
|
||||
$countimages = self::START_NUM_ITEMS;
|
||||
}
|
||||
$itemrepeatsatstart = $countimages;
|
||||
|
||||
$imagerepeats = optional_param('noitems', $itemrepeatsatstart, PARAM_INT);
|
||||
$addfields = optional_param('additems', false, PARAM_BOOL);
|
||||
if ($addfields) {
|
||||
$imagerepeats += self::ADD_NUM_ITEMS;
|
||||
}
|
||||
return array($itemrepeatsatstart, $imagerepeats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performce the needed JS setup for this question type.
|
||||
*/
|
||||
abstract public function js_call();
|
||||
|
||||
/**
|
||||
* Checks to see if a file has been uploaded.
|
||||
*
|
||||
* @param string $draftitemid The draft id
|
||||
* @return bool True if files exist, false if not.
|
||||
*/
|
||||
public static function file_uploaded($draftitemid) {
|
||||
$draftareafiles = file_get_drafarea_files($draftitemid);
|
||||
do {
|
||||
$draftareafile = array_shift($draftareafiles->list);
|
||||
} while ($draftareafile !== null && $draftareafile->filename == '.');
|
||||
if ($draftareafile === null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Language file Drag and Drop image or text.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['addmoredropzones'] = 'Blanks for {no} more drop zones';
|
||||
$string['addmoreimages'] = 'Blanks for {no} more draggable items';
|
||||
$string['answer'] = 'Answer';
|
||||
$string['bgimage'] = 'Background image';
|
||||
$string['correctansweris'] = 'The correct answer is: {$a}';
|
||||
$string['draggableimage'] = 'Draggable image';
|
||||
$string['draggableitem'] = 'Draggable item';
|
||||
$string['draggableitems'] = 'Draggable items';
|
||||
$string['draggableitemheader'] = 'Draggable item {$a}';
|
||||
$string['draggableitemtype'] = 'Type';
|
||||
$string['draggableword'] = 'Draggable text';
|
||||
$string['dropbackground'] = 'Background image for dragging markers onto';
|
||||
$string['dropzone'] = 'Drop zone {$a}';
|
||||
$string['dropzoneheader'] = 'Drop zones';
|
||||
$string['formerror_disallowedtags'] = 'You have used html tags here that are not allowed in a draggable text drag item type.';
|
||||
$string['formerror_noallowedtags'] = 'No html tags are allowed in this text which is the alt text for a draggable image';
|
||||
$string['formerror_noytop'] = 'You must provide a value for the y coords for the top left corner of this drop area. You can drag and drop the drop area above to set the coordinates or enter them manually here.';
|
||||
$string['formerror_noxleft'] = 'You must provide a value for the x coords for the top left corner of this drop area. You can drag and drop the drop area above to set the coordinates or enter them manually here.';
|
||||
$string['formerror_nofile'] = 'You need to upload or select a file to use here.';
|
||||
$string['formerror_nofile3'] = 'You need to select an image file here, or delete the associated label and uncheck the infinite checkbox.';
|
||||
$string['formerror_notintytop'] = 'The y coords must be an integer.';
|
||||
$string['formerror_notintxleft'] = 'The x coords must be an integer.';
|
||||
$string['formerror_multipledraginstance'] = 'You have selected this image {$a} more than once as the correct choice for a drop zone but it is not marked as being an infinite drag item.';
|
||||
$string['formerror_multipledraginstance2'] = 'You have selected this image more than once as the correct choice for a drop zone but it is not marked as being an infinite drag item.';
|
||||
$string['formerror_noimageselected'] = 'You need to select a drag item to be the correct choice for this drop zone.';
|
||||
$string['formerror_nobgimage'] = 'You need to select an image to use as the background for the drag and drop area.';
|
||||
$string['infinite'] = 'Infinite';
|
||||
$string['label'] = 'Text';
|
||||
$string['nolabel'] = 'No label text';
|
||||
$string['pleasedraganimagetoeachdropregion'] = 'Your answer is not complete, please drag an item to each drop region.';
|
||||
$string['pluginname'] = 'Drag and drop onto image';
|
||||
$string['pluginname_help'] = 'Select a background image file, select draggable images or enter text and define the drop zones on the background image to which they must be dragged.';
|
||||
$string['pluginname_link'] = 'question/type/ddimageortext';
|
||||
$string['pluginnameadding'] = 'Adding drag and drop onto image';
|
||||
$string['pluginnameediting'] = 'Editing drag and drop onto image';
|
||||
$string['pluginnamesummary'] = 'Images or text labels are dragged and dropped into drop zones on a background image.';
|
||||
$string['previewareaheader'] = 'Preview';
|
||||
$string['previewareamessage'] = 'Select a background image file and select draggable images or just enter text that will be made draggable. Then choose a drag item for each \'drop zone\', and drag the drag item to where the student should drag it to.';
|
||||
$string['refresh'] = 'Refresh preview';
|
||||
$string['shuffleimages'] = 'Shuffle drag items each time question is attempted';
|
||||
$string['summarisechoice'] = '{$a->no}. {$a->text}';
|
||||
$string['summariseplace'] = '{$a->no}. {$a->text}';
|
||||
$string['summarisechoiceno'] = 'Item {$a}';
|
||||
$string['summariseplaceno'] = 'Drop zone {$a}';
|
||||
$string['xleft'] = 'Left';
|
||||
$string['ytop'] = 'Top';
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Serve question type files
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright Dongsheng Cai <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Checks file access for ddimageortext questions.
|
||||
*
|
||||
* @param object $course The course we are in
|
||||
* @param object $cm Course module
|
||||
* @param object $context The context object
|
||||
* @param string $filearea the name of the file area.
|
||||
* @param array $args the remaining bits of the file path.
|
||||
* @param bool $forcedownload whether the user must be forced to download the file.
|
||||
* @param array $options additional options affecting the file serving
|
||||
*/
|
||||
function qtype_ddimageortext_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
question_pluginfile($course, $context, 'qtype_ddimageortext', $filearea, $args, $forcedownload, $options);
|
||||
}
|
||||
|
After Width: | Height: | Size: 267 B |
@@ -0,0 +1,153 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop onto image question definition class.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php');
|
||||
|
||||
|
||||
/**
|
||||
* Represents a drag-and-drop onto image question.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_question extends qtype_ddtoimage_question_base {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents one of the choices (draggable images).
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_drag_item {
|
||||
/** @var int Drag item id */
|
||||
public $id;
|
||||
|
||||
/** @var string Text for the drag item */
|
||||
public $text;
|
||||
|
||||
/** @var int Number of the item */
|
||||
public $no;
|
||||
|
||||
/** @var int Group of the item */
|
||||
public $group;
|
||||
|
||||
/** @var bool If the drag item can be used multiple times or not */
|
||||
public $infinite;
|
||||
|
||||
/**
|
||||
* Drag item object setup.
|
||||
*
|
||||
* @param string $alttextlabel The alt text of the drag item
|
||||
* @param int $no Which number drag item this is
|
||||
* @param int $group Group of the drag item
|
||||
* @param bool $infinite True if the item can be used an unlimited number of times
|
||||
* @param int $id id of the item
|
||||
*/
|
||||
public function __construct($alttextlabel, $no, $group = 1, $infinite = false, $id = 0) {
|
||||
$this->id = $id;
|
||||
$this->text = $alttextlabel;
|
||||
$this->no = $no;
|
||||
$this->group = $group;
|
||||
$this->infinite = $infinite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the group of this item.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function choice_group() {
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates summary text of for the drag item.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function summarise() {
|
||||
if (trim($this->text) != '') {
|
||||
return get_string('summarisechoice', 'qtype_ddimageortext', $this);
|
||||
} else {
|
||||
return get_string('summarisechoiceno', 'qtype_ddimageortext', $this->no);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents one of the places (drop zones).
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_drop_zone {
|
||||
/** @var int Number of the item */
|
||||
public $no;
|
||||
|
||||
/** @var string Alt text for the drop zone item */
|
||||
public $text;
|
||||
|
||||
/** @var int Group of the item */
|
||||
public $group;
|
||||
|
||||
/** @var array X and Y location of the drop zone */
|
||||
public $xy;
|
||||
|
||||
/**
|
||||
* Create a drop zone object.
|
||||
*
|
||||
* @param string $alttextlabel The alt text of the drop zone
|
||||
* @param int $no Which number drop zone this is
|
||||
* @param int $group Group of the drop zone
|
||||
* @param int $x X location
|
||||
* @param int $y Y location
|
||||
*/
|
||||
public function __construct($alttextlabel, $no, $group = 1, $x = '', $y = '') {
|
||||
$this->no = $no;
|
||||
$this->text = $alttextlabel;
|
||||
$this->group = $group;
|
||||
$this->xy = array($x, $y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates summary text of for the drop zone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function summarise() {
|
||||
if (trim($this->text) != '') {
|
||||
$summariseplace =
|
||||
get_string('summariseplace', 'qtype_ddimageortext', $this);
|
||||
} else {
|
||||
$summariseplace =
|
||||
get_string('summariseplaceno', 'qtype_ddimageortext', $this->no);
|
||||
}
|
||||
return $summariseplace;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop onto image question definition class.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/gapselect/questionbase.php');
|
||||
|
||||
|
||||
/**
|
||||
* Represents a drag-and-drop onto image question.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddtoimage_question_base extends qtype_gapselect_question_base {
|
||||
public function clear_wrong_from_response(array $response) {
|
||||
foreach ($this->places as $place => $notused) {
|
||||
if (array_key_exists($this->field($place), $response) &&
|
||||
$response[$this->field($place)] != $this->get_right_choice_for($place)) {
|
||||
$response[$this->field($place)] = '';
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function get_right_choice_for($placeno) {
|
||||
$place = $this->places[$placeno];
|
||||
foreach ($this->choiceorder[$place->group] as $choicekey => $choiceid) {
|
||||
if ($this->rightchoices[$placeno] == $choiceid) {
|
||||
return $choicekey;
|
||||
}
|
||||
}
|
||||
}
|
||||
public function summarise_response(array $response) {
|
||||
$allblank = true;
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
$summariseplace = $place->summarise();
|
||||
if (array_key_exists($this->field($placeno), $response) &&
|
||||
$response[$this->field($placeno)]) {
|
||||
$selected = $this->get_selected_choice($place->group,
|
||||
$response[$this->field($placeno)]);
|
||||
$summarisechoice = $selected->summarise();
|
||||
$allblank = false;
|
||||
} else {
|
||||
$summarisechoice = '';
|
||||
}
|
||||
$choices[] = "$summariseplace -> {{$summarisechoice}}";
|
||||
}
|
||||
if ($allblank) {
|
||||
return null;
|
||||
}
|
||||
return implode(' ', $choices);
|
||||
}
|
||||
|
||||
public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
|
||||
if ($filearea == 'bgimage' || $filearea == 'dragimage') {
|
||||
$validfilearea = true;
|
||||
} else {
|
||||
$validfilearea = false;
|
||||
}
|
||||
if ($component == 'qtype_ddimageortext' && $validfilearea) {
|
||||
$question = $qa->get_question();
|
||||
$itemid = reset($args);
|
||||
if ($filearea == 'bgimage') {
|
||||
return $itemid == $question->id;
|
||||
} else if ($filearea == 'dragimage') {
|
||||
foreach ($question->choices as $group) {
|
||||
foreach ($group as $drag) {
|
||||
if ($drag->id == $itemid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return parent::check_file_access($qa, $options, $component,
|
||||
$filearea, $args, $forcedownload);
|
||||
}
|
||||
}
|
||||
public function get_validation_error(array $response) {
|
||||
if ($this->is_complete_response($response)) {
|
||||
return '';
|
||||
}
|
||||
return get_string('pleasedraganimagetoeachdropregion', 'qtype_ddimageortext');
|
||||
}
|
||||
|
||||
public function classify_response(array $response) {
|
||||
$parts = array();
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
$group = $place->group;
|
||||
if (!array_key_exists($this->field($placeno), $response) ||
|
||||
!$response[$this->field($placeno)]) {
|
||||
$parts[$placeno] = question_classified_response::no_response();
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldname = $this->field($placeno);
|
||||
$choicekey = $this->choiceorder[$group][$response[$fieldname]];
|
||||
$choice = $this->choices[$group][$choicekey];
|
||||
|
||||
$correct = $this->get_right_choice_for($placeno) == $response[$fieldname];
|
||||
if ($correct) {
|
||||
$grade = 1;
|
||||
} else {
|
||||
$grade = 0;
|
||||
}
|
||||
$parts[$placeno] = new question_classified_response($choice->no, $choice->summarise(), $grade);
|
||||
}
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public function get_random_guess_score() {
|
||||
$accum = 0;
|
||||
|
||||
foreach ($this->places as $place) {
|
||||
foreach ($this->choices[$place->group] as $choice) {
|
||||
if ($choice->infinite) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
$accum += 1 / count($this->choices[$place->group]);
|
||||
}
|
||||
|
||||
return $accum / count($this->places);
|
||||
}
|
||||
|
||||
|
||||
public function get_question_summary() {
|
||||
$summary = '';
|
||||
if (!html_is_blank($this->questiontext)) {
|
||||
$question = $this->html_to_text($this->questiontext, $this->questiontextformat);
|
||||
$summary .= $question . '; ';
|
||||
}
|
||||
$places = array();
|
||||
foreach ($this->places as $place) {
|
||||
$cs = array();
|
||||
foreach ($this->choices[$place->group] as $choice) {
|
||||
$cs[] = $choice->summarise();
|
||||
}
|
||||
$places[] = '[[' . $place->summarise() . ']] -> {' . implode(' / ', $cs) . '}';
|
||||
}
|
||||
$summary .= implode('; ', $places);
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Question type class for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/questiontypebase.php');
|
||||
|
||||
define('QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH', 600);
|
||||
define('QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT', 400);
|
||||
define('QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH', 150);
|
||||
define('QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT', 100);
|
||||
|
||||
/**
|
||||
* The drag-and-drop onto image question type class.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext extends qtype_ddtoimage_base {
|
||||
|
||||
protected function make_choice($dragdata) {
|
||||
return new qtype_ddimageortext_drag_item($dragdata->label, $dragdata->no,
|
||||
$dragdata->draggroup, $dragdata->infinite, $dragdata->id);
|
||||
}
|
||||
|
||||
protected function make_place($dropzonedata) {
|
||||
return new qtype_ddimageortext_drop_zone($dropzonedata->label, $dropzonedata->no,
|
||||
$dropzonedata->group,
|
||||
$dropzonedata->xleft, $dropzonedata->ytop);
|
||||
}
|
||||
|
||||
protected function make_hint($hint) {
|
||||
return question_hint_with_parts::load_from_record($hint);
|
||||
}
|
||||
|
||||
public function save_question_options($formdata) {
|
||||
global $DB, $USER;
|
||||
$context = $formdata->context;
|
||||
|
||||
$options = $DB->get_record('qtype_ddimageortext', array('questionid' => $formdata->id));
|
||||
if (!$options) {
|
||||
$options = new stdClass();
|
||||
$options->questionid = $formdata->id;
|
||||
$options->correctfeedback = '';
|
||||
$options->partiallycorrectfeedback = '';
|
||||
$options->incorrectfeedback = '';
|
||||
$options->id = $DB->insert_record('qtype_ddimageortext', $options);
|
||||
}
|
||||
|
||||
$options->shuffleanswers = !empty($formdata->shuffleanswers);
|
||||
$options = $this->save_combined_feedback_helper($options, $formdata, $context, true);
|
||||
$this->save_hints($formdata, true);
|
||||
$DB->update_record('qtype_ddimageortext', $options);
|
||||
$DB->delete_records('qtype_ddimageortext_drops', array('questionid' => $formdata->id));
|
||||
foreach (array_keys($formdata->drops) as $dropno) {
|
||||
if ($formdata->drops[$dropno]['choice'] == 0) {
|
||||
continue;
|
||||
}
|
||||
$drop = new stdClass();
|
||||
$drop->questionid = $formdata->id;
|
||||
$drop->no = $dropno + 1;
|
||||
$drop->xleft = $formdata->drops[$dropno]['xleft'];
|
||||
$drop->ytop = $formdata->drops[$dropno]['ytop'];
|
||||
$drop->choice = $formdata->drops[$dropno]['choice'];
|
||||
$drop->label = $formdata->drops[$dropno]['droplabel'];
|
||||
|
||||
$DB->insert_record('qtype_ddimageortext_drops', $drop);
|
||||
}
|
||||
|
||||
// An array of drag no -> drag id.
|
||||
$olddragids = $DB->get_records_menu('qtype_ddimageortext_drags',
|
||||
array('questionid' => $formdata->id),
|
||||
'', 'no, id');
|
||||
foreach (array_keys($formdata->drags) as $dragno) {
|
||||
$info = file_get_draft_area_info($formdata->dragitem[$dragno]);
|
||||
if ($info['filecount'] > 0 || (trim($formdata->draglabel[$dragno]) != '')) {
|
||||
$draftitemid = $formdata->dragitem[$dragno];
|
||||
|
||||
$drag = new stdClass();
|
||||
$drag->questionid = $formdata->id;
|
||||
$drag->no = $dragno + 1;
|
||||
$drag->draggroup = $formdata->drags[$dragno]['draggroup'];
|
||||
$drag->infinite = empty($formdata->drags[$dragno]['infinite']) ? 0 : 1;
|
||||
$drag->label = $formdata->draglabel[$dragno];
|
||||
|
||||
if (isset($olddragids[$dragno + 1])) {
|
||||
$drag->id = $olddragids[$dragno + 1];
|
||||
unset($olddragids[$dragno + 1]);
|
||||
$DB->update_record('qtype_ddimageortext_drags', $drag);
|
||||
} else {
|
||||
$drag->id = $DB->insert_record('qtype_ddimageortext_drags', $drag);
|
||||
}
|
||||
|
||||
if ($formdata->drags[$dragno]['dragitemtype'] == 'image') {
|
||||
self::constrain_image_size_in_draft_area($draftitemid,
|
||||
QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH,
|
||||
QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT);
|
||||
file_save_draft_area_files($draftitemid, $formdata->context->id,
|
||||
'qtype_ddimageortext', 'dragimage', $drag->id,
|
||||
array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
|
||||
} else {
|
||||
// Delete any existing files for draggable text item type.
|
||||
$fs = get_file_storage();
|
||||
$fs->delete_area_files($formdata->context->id, 'qtype_ddimageortext',
|
||||
'dragimage', $drag->id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!empty($olddragids)) {
|
||||
list($sql, $params) = $DB->get_in_or_equal(array_values($olddragids));
|
||||
$DB->delete_records_select('qtype_ddimageortext_drags', "id $sql", $params);
|
||||
}
|
||||
|
||||
self::constrain_image_size_in_draft_area($formdata->bgimage,
|
||||
QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH,
|
||||
QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT);
|
||||
file_save_draft_area_files($formdata->bgimage, $formdata->context->id,
|
||||
'qtype_ddimageortext', 'bgimage', $formdata->id,
|
||||
array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
|
||||
}
|
||||
public function move_files($questionid, $oldcontextid, $newcontextid) {
|
||||
global $DB;
|
||||
$fs = get_file_storage();
|
||||
|
||||
parent::move_files($questionid, $oldcontextid, $newcontextid);
|
||||
$fs->move_area_files_to_new_context($oldcontextid,
|
||||
$newcontextid, 'qtype_ddimageortext', 'bgimage', $questionid);
|
||||
$dragids = $DB->get_records_menu('qtype_ddimageortext_drags',
|
||||
array('questionid' => $questionid), 'id', 'id,1');
|
||||
foreach ($dragids as $dragid => $notused) {
|
||||
$fs->move_area_files_to_new_context($oldcontextid,
|
||||
$newcontextid, 'qtype_ddimageortext', 'dragimage', $dragid);
|
||||
}
|
||||
|
||||
$this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
|
||||
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the files belonging to this question.
|
||||
* @param int $questionid the question being deleted.
|
||||
* @param int $contextid the context the question is in.
|
||||
*/
|
||||
|
||||
protected function delete_files($questionid, $contextid) {
|
||||
global $DB;
|
||||
$fs = get_file_storage();
|
||||
|
||||
parent::delete_files($questionid, $contextid);
|
||||
|
||||
$dragids = $DB->get_records_menu('qtype_ddimageortext_drags',
|
||||
array('questionid' => $questionid), 'id', 'id,1');
|
||||
foreach ($dragids as $dragid => $notused) {
|
||||
$fs->delete_area_files($contextid, 'qtype_ddimageortext', 'dragimage', $dragid);
|
||||
}
|
||||
|
||||
$this->delete_files_in_combined_feedback($questionid, $contextid);
|
||||
$this->delete_files_in_hints($questionid, $contextid);
|
||||
}
|
||||
|
||||
|
||||
public function export_to_xml($question, qformat_xml $format, $extra = null) {
|
||||
$fs = get_file_storage();
|
||||
$contextid = $question->contextid;
|
||||
$output = '';
|
||||
|
||||
if ($question->options->shuffleanswers) {
|
||||
$output .= " <shuffleanswers/>\n";
|
||||
}
|
||||
$output .= $format->write_combined_feedback($question->options,
|
||||
$question->id,
|
||||
$question->contextid);
|
||||
$files = $fs->get_area_files($contextid, 'qtype_ddimageortext', 'bgimage', $question->id);
|
||||
$output .= " ".$this->write_files($files, 2)."\n";;
|
||||
|
||||
foreach ($question->options->drags as $drag) {
|
||||
$files =
|
||||
$fs->get_area_files($contextid, 'qtype_ddimageortext', 'dragimage', $drag->id);
|
||||
$output .= " <drag>\n";
|
||||
$output .= " <no>{$drag->no}</no>\n";
|
||||
$output .= $format->writetext($drag->label, 3)."\n";
|
||||
$output .= " <draggroup>{$drag->draggroup}</draggroup>\n";
|
||||
if ($drag->infinite) {
|
||||
$output .= " <infinite/>\n";
|
||||
}
|
||||
$output .= $this->write_files($files, 3);
|
||||
$output .= " </drag>\n";
|
||||
}
|
||||
foreach ($question->options->drops as $drop) {
|
||||
$output .= " <drop>\n";
|
||||
$output .= $format->writetext($drop->label, 3);
|
||||
$output .= " <no>{$drop->no}</no>\n";
|
||||
$output .= " <choice>{$drop->choice}</choice>\n";
|
||||
$output .= " <xleft>{$drop->xleft}</xleft>\n";
|
||||
$output .= " <ytop>{$drop->ytop}</ytop>\n";
|
||||
$output .= " </drop>\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
|
||||
if (!isset($data['@']['type']) || $data['@']['type'] != 'ddimageortext') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$question = $format->import_headers($data);
|
||||
$question->qtype = 'ddimageortext';
|
||||
|
||||
$question->shuffleanswers = array_key_exists('shuffleanswers',
|
||||
$format->getpath($data, array('#'), array()));
|
||||
|
||||
$filexml = $format->getpath($data, array('#', 'file'), array());
|
||||
$question->bgimage = $format->import_files_as_draft($filexml);
|
||||
$drags = $data['#']['drag'];
|
||||
$question->drags = array();
|
||||
|
||||
foreach ($drags as $dragxml) {
|
||||
$dragno = $format->getpath($dragxml, array('#', 'no', 0, '#'), 0);
|
||||
$dragindex = $dragno - 1;
|
||||
$question->drags[$dragindex] = array();
|
||||
$question->draglabel[$dragindex] =
|
||||
$format->getpath($dragxml, array('#', 'text', 0, '#'), '', true);
|
||||
$question->drags[$dragindex]['infinite'] = array_key_exists('infinite', $dragxml['#']);
|
||||
$question->drags[$dragindex]['draggroup'] =
|
||||
$format->getpath($dragxml, array('#', 'draggroup', 0, '#'), 1);
|
||||
$filexml = $format->getpath($dragxml, array('#', 'file'), array());
|
||||
$question->dragitem[$dragindex] = $format->import_files_as_draft($filexml);
|
||||
if (count($filexml)) {
|
||||
$question->drags[$dragindex]['dragitemtype'] = 'image';
|
||||
} else {
|
||||
$question->drags[$dragindex]['dragitemtype'] = 'word';
|
||||
}
|
||||
}
|
||||
|
||||
$drops = $data['#']['drop'];
|
||||
$question->drops = array();
|
||||
foreach ($drops as $dropxml) {
|
||||
$dropno = $format->getpath($dropxml, array('#', 'no', 0, '#'), 0);
|
||||
$dropindex = $dropno - 1;
|
||||
$question->drops[$dropindex] = array();
|
||||
$question->drops[$dropindex]['choice'] =
|
||||
$format->getpath($dropxml, array('#', 'choice', 0, '#'), 0);
|
||||
$question->drops[$dropindex]['droplabel'] =
|
||||
$format->getpath($dropxml, array('#', 'text', 0, '#'), '', true);
|
||||
$question->drops[$dropindex]['xleft'] =
|
||||
$format->getpath($dropxml, array('#', 'xleft', 0, '#'), '');
|
||||
$question->drops[$dropindex]['ytop'] =
|
||||
$format->getpath($dropxml, array('#', 'ytop', 0, '#'), '');
|
||||
}
|
||||
|
||||
$format->import_combined_feedback($question, $data, true);
|
||||
$format->import_hints($question, $data, true, false,
|
||||
$format->get_format($question->questiontextformat));
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Question type class for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/lib.php');
|
||||
require_once($CFG->dirroot . '/question/format/xml/format.php');
|
||||
require_once($CFG->dirroot . '/question/type/gapselect/questiontypebase.php');
|
||||
|
||||
/**
|
||||
* The drag-and-drop onto image question type class.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddtoimage_base extends question_type {
|
||||
/**
|
||||
* Returns the choice group key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function choice_group_key() {
|
||||
return 'draggroup';
|
||||
}
|
||||
|
||||
public function get_question_options($question) {
|
||||
global $DB;
|
||||
$dbprefix = 'qtype_'.$this->name();
|
||||
$question->options = $DB->get_record($dbprefix,
|
||||
array('questionid' => $question->id), '*', MUST_EXIST);
|
||||
$question->options->drags = $DB->get_records($dbprefix.'_drags',
|
||||
array('questionid' => $question->id), 'no ASC', '*');
|
||||
$question->options->drops = $DB->get_records($dbprefix.'_drops',
|
||||
array('questionid' => $question->id), 'no ASC', '*');
|
||||
parent::get_question_options($question);
|
||||
}
|
||||
|
||||
protected function initialise_question_instance(question_definition $question, $questiondata) {
|
||||
parent::initialise_question_instance($question, $questiondata);
|
||||
$question->shufflechoices = $questiondata->options->shuffleanswers;
|
||||
|
||||
$this->initialise_combined_feedback($question, $questiondata, true);
|
||||
|
||||
$question->choices = array();
|
||||
$choiceindexmap = array();
|
||||
|
||||
// Store the choices in arrays by group.
|
||||
// This code is weird. The first choice in each group gets key 1 in the
|
||||
// $question->choices[$choice->choice_group()] array, and the others get
|
||||
// key $choice->no. Therefore you need to think carefully whether you
|
||||
// are using the key, or $choice->no. This is presumably a mistake, but
|
||||
// one that is now essentially un-fixable, since many questions of this
|
||||
// type have been attempted, and theys keys get stored in the attempt data.
|
||||
foreach ($questiondata->options->drags as $dragdata) {
|
||||
|
||||
$choice = $this->make_choice($dragdata);
|
||||
|
||||
if (array_key_exists($choice->choice_group(), $question->choices)) {
|
||||
$question->choices[$choice->choice_group()][$dragdata->no] = $choice;
|
||||
} else {
|
||||
$question->choices[$choice->choice_group()][1] = $choice;
|
||||
}
|
||||
|
||||
end($question->choices[$choice->choice_group()]);
|
||||
$choiceindexmap[$dragdata->no] = array($choice->choice_group(),
|
||||
key($question->choices[$choice->choice_group()]));
|
||||
}
|
||||
|
||||
$question->places = array();
|
||||
$question->rightchoices = array();
|
||||
|
||||
$i = 1;
|
||||
|
||||
foreach ($questiondata->options->drops as $dropdata) {
|
||||
list($group, $choiceindex) = $choiceindexmap[$dropdata->choice];
|
||||
$dropdata->group = $group;
|
||||
$question->places[$dropdata->no] = $this->make_place($dropdata);
|
||||
$question->rightchoices[$dropdata->no] = $choiceindex;
|
||||
}
|
||||
}
|
||||
|
||||
public static function constrain_image_size_in_draft_area($draftitemid, $maxwidth, $maxheight) {
|
||||
global $USER;
|
||||
$usercontext = context_user::instance($USER->id);
|
||||
$fs = get_file_storage();
|
||||
$draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
|
||||
if ($draftfiles) {
|
||||
foreach ($draftfiles as $file) {
|
||||
if ($file->is_directory()) {
|
||||
continue;
|
||||
}
|
||||
$imageinfo = $file->get_imageinfo();
|
||||
$width = $imageinfo['width'];
|
||||
$height = $imageinfo['height'];
|
||||
$mimetype = $imageinfo['mimetype'];
|
||||
switch ($mimetype) {
|
||||
case 'image/jpeg' :
|
||||
$quality = 80;
|
||||
break;
|
||||
case 'image/png' :
|
||||
$quality = 8;
|
||||
break;
|
||||
default :
|
||||
$quality = null;
|
||||
}
|
||||
$newwidth = min($maxwidth, $width);
|
||||
$newheight = min($maxheight, $height);
|
||||
if ($newwidth != $width || $newheight != $height) {
|
||||
$newimagefilename = $file->get_filename();
|
||||
$newimagefilename =
|
||||
preg_replace('!\.!', "_{$newwidth}x{$newheight}.", $newimagefilename, 1);
|
||||
$newrecord = new stdClass();
|
||||
$newrecord->contextid = $usercontext->id;
|
||||
$newrecord->component = 'user';
|
||||
$newrecord->filearea = 'draft';
|
||||
$newrecord->itemid = $draftitemid;
|
||||
$newrecord->filepath = '/';
|
||||
$newrecord->filename = $newimagefilename;
|
||||
$fs->convert_image($newrecord, $file, $newwidth, $newheight, true, $quality);
|
||||
$file->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert files into text output in the given format.
|
||||
* This method is copied from qformat_default as a quick fix, as the method there is
|
||||
* protected.
|
||||
* @param array $files
|
||||
* @param int $indent Number of spaces to indent
|
||||
* @return string $string
|
||||
*/
|
||||
public function write_files($files, $indent) {
|
||||
if (empty($files)) {
|
||||
return '';
|
||||
}
|
||||
$string = '';
|
||||
foreach ($files as $file) {
|
||||
if ($file->is_directory()) {
|
||||
continue;
|
||||
}
|
||||
$string .= str_repeat(' ', $indent);
|
||||
$string .= '<file name="' . $file->get_filename() . '" encoding="base64">';
|
||||
$string .= base64_encode($file->get_content());
|
||||
$string .= "</file>\n";
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
public function get_possible_responses($questiondata) {
|
||||
$question = $this->make_question($questiondata);
|
||||
|
||||
$parts = array();
|
||||
foreach ($question->places as $placeno => $place) {
|
||||
$choices = array();
|
||||
|
||||
foreach ($question->choices[$place->group] as $i => $choice) {
|
||||
$correct = $question->rightchoices[$placeno] == $i;
|
||||
$choices[$choice->no] = new question_possible_response($choice->summarise(), $correct ? 1 : 0);
|
||||
}
|
||||
$choices[null] = question_possible_response::no_response();
|
||||
|
||||
$parts[$placeno] = $choices;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public function get_random_guess_score($questiondata) {
|
||||
$question = $this->make_question($questiondata);
|
||||
return $question->get_random_guess_score();
|
||||
}
|
||||
public function delete_question($questionid, $contextid) {
|
||||
global $DB;
|
||||
$DB->delete_records('qtype_'.$this->name(), array('questionid' => $questionid));
|
||||
$DB->delete_records('qtype_'.$this->name().'_drags', array('questionid' => $questionid));
|
||||
$DB->delete_records('qtype_'.$this->name().'_drops', array('questionid' => $questionid));
|
||||
return parent::delete_question($questionid, $contextid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop onto image question renderer class.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/rendererbase.php');
|
||||
|
||||
/**
|
||||
* Generates the output for drag-and-drop onto image questions.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_renderer extends qtype_ddtoimage_renderer_base {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop onto image question renderer class.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Generates the output for drag-and-drop onto image questions.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddtoimage_renderer_base extends qtype_with_combined_feedback_renderer {
|
||||
|
||||
public function clear_wrong(question_attempt $qa) {
|
||||
$question = $qa->get_question();
|
||||
$response = $qa->get_last_qt_data();
|
||||
|
||||
if (!empty($response)) {
|
||||
$cleanresponse = $question->clear_wrong_from_response($response);
|
||||
} else {
|
||||
$cleanresponse = $response;
|
||||
}
|
||||
$cleanresponsehtml = '';
|
||||
foreach ($cleanresponse as $fieldname => $value) {
|
||||
list (, $html) = $this->hidden_field_for_qt_var($qa, $fieldname, $value);
|
||||
$cleanresponsehtml .= $html;
|
||||
}
|
||||
return $cleanresponsehtml;
|
||||
}
|
||||
|
||||
public function formulation_and_controls(question_attempt $qa,
|
||||
question_display_options $options) {
|
||||
global $PAGE;
|
||||
|
||||
$question = $qa->get_question();
|
||||
$response = $qa->get_last_qt_data();
|
||||
|
||||
$questiontext = $question->format_questiontext($qa);
|
||||
|
||||
$output = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
|
||||
|
||||
$bgimage = self::get_url_for_image($qa, 'bgimage');
|
||||
|
||||
$img = html_writer::empty_tag('img', array(
|
||||
'src' => $bgimage, 'class' => 'dropbackground',
|
||||
'alt' => get_string('dropbackground', 'qtype_ddimageortext')));
|
||||
|
||||
$droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
|
||||
|
||||
$dragimagehomes = '';
|
||||
foreach ($question->choices as $groupno => $group) {
|
||||
$dragimagehomesgroup = '';
|
||||
$orderedgroup = $question->get_ordered_choices($groupno);
|
||||
foreach ($orderedgroup as $choiceno => $dragimage) {
|
||||
$dragimageurl = self::get_url_for_image($qa, 'dragimage', $dragimage->id);
|
||||
$classes = array("group{$groupno}",
|
||||
'draghome',
|
||||
"dragitemhomes{$dragimage->no}",
|
||||
"choice{$choiceno}");
|
||||
if ($dragimage->infinite) {
|
||||
$classes[] = 'infinite';
|
||||
}
|
||||
if ($dragimageurl === null) {
|
||||
$classes[] = 'yui3-cssfonts';
|
||||
$dragimagehomesgroup .= html_writer::tag('div', $dragimage->text,
|
||||
array('src' => $dragimageurl, 'class' => join(' ', $classes)));
|
||||
} else {
|
||||
$dragimagehomesgroup .= html_writer::empty_tag('img',
|
||||
array('src' => $dragimageurl, 'alt' => $dragimage->text,
|
||||
'class' => join(' ', $classes)));
|
||||
}
|
||||
}
|
||||
$dragimagehomes .= html_writer::tag('div', $dragimagehomesgroup,
|
||||
array('class' => 'dragitemgroup' . $groupno));
|
||||
}
|
||||
|
||||
$dragitemsclass = 'dragitems';
|
||||
if ($options->readonly) {
|
||||
$dragitemsclass .= ' readonly';
|
||||
}
|
||||
$dragitems = html_writer::tag('div', $dragimagehomes, array('class' => $dragitemsclass));
|
||||
$dropzones = html_writer::tag('div', '', array('class' => 'dropzones'));
|
||||
|
||||
$hiddens = '';
|
||||
foreach ($question->places as $placeno => $place) {
|
||||
$varname = $question->field($placeno);
|
||||
list($fieldname, $html) = $this->hidden_field_for_qt_var($qa, $varname);
|
||||
$hiddens .= $html;
|
||||
$question->places[$placeno]->fieldname = $fieldname;
|
||||
}
|
||||
$output .= html_writer::tag('div',
|
||||
$droparea . $dragitems . $dropzones . $hiddens, array('class' => 'ddarea'));
|
||||
$topnode = 'div#q'.$qa->get_slot().' div.ddarea';
|
||||
$params = array('drops' => $question->places,
|
||||
'topnode' => $topnode,
|
||||
'readonly' => $options->readonly);
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddimageortext-dd',
|
||||
'M.qtype_ddimageortext.init_question',
|
||||
array($params));
|
||||
|
||||
if ($qa->get_state() == question_state::$invalid) {
|
||||
$output .= html_writer::nonempty_tag('div',
|
||||
$question->get_validation_error($qa->get_last_qt_data()),
|
||||
array('class' => 'validationerror'));
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL for an image
|
||||
*
|
||||
* @param object $qa Question attempt object
|
||||
* @param string $filearea File area descriptor
|
||||
* @param int $itemid Item id to get
|
||||
* @return string Output url, or null if not found
|
||||
*/
|
||||
protected static function get_url_for_image(question_attempt $qa, $filearea, $itemid = 0) {
|
||||
$question = $qa->get_question();
|
||||
$qubaid = $qa->get_usage_id();
|
||||
$slot = $qa->get_slot();
|
||||
$fs = get_file_storage();
|
||||
if ($filearea == 'bgimage') {
|
||||
$itemid = $question->id;
|
||||
}
|
||||
$componentname = $question->qtype->plugin_name();
|
||||
$draftfiles = $fs->get_area_files($question->contextid, $componentname,
|
||||
$filearea, $itemid, 'id');
|
||||
if ($draftfiles) {
|
||||
foreach ($draftfiles as $file) {
|
||||
if ($file->is_directory()) {
|
||||
continue;
|
||||
}
|
||||
$url = moodle_url::make_pluginfile_url($question->contextid, $componentname,
|
||||
$filearea, "$qubaid/$slot/{$itemid}", '/',
|
||||
$file->get_filename());
|
||||
return $url->out();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hidden field for a qt variable
|
||||
*
|
||||
* @param object $qa Question attempt object
|
||||
* @param string $varname The hidden var name
|
||||
* @param string $value The hidden value
|
||||
* @param array $classes Any additional css classes to apply
|
||||
* @return array Array with field name and the html of the tag
|
||||
*/
|
||||
protected function hidden_field_for_qt_var(question_attempt $qa, $varname, $value = null,
|
||||
$classes = null) {
|
||||
if ($value === null) {
|
||||
$value = $qa->get_last_qt_var($varname);
|
||||
}
|
||||
$fieldname = $qa->get_qt_field_name($varname);
|
||||
$attributes = array('type' => 'hidden',
|
||||
'id' => str_replace(':', '_', $fieldname),
|
||||
'name' => $fieldname,
|
||||
'value' => $value);
|
||||
if ($classes !== null) {
|
||||
$attributes['class'] = join(' ', $classes);
|
||||
}
|
||||
return array($fieldname, html_writer::empty_tag('input', $attributes)."\n");
|
||||
}
|
||||
|
||||
public function specific_feedback(question_attempt $qa) {
|
||||
return $this->combined_feedback($qa);
|
||||
}
|
||||
|
||||
public function correct_response(question_attempt $qa) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
.que.ddimageortext .qtext {
|
||||
margin-bottom: 0.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.que.ddimageortext div.droparea img, form.mform fieldset#id_previewareaheader div.droparea img {
|
||||
border: 1px solid #000000;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.que.ddimageortext .draghome, form.mform fieldset#id_previewareaheader .draghome {
|
||||
vertical-align: top;
|
||||
margin: 5px;
|
||||
visibility : hidden;
|
||||
}
|
||||
|
||||
.que.ddimageortext div.draghome, form.mform fieldset#id_previewareaheader div.draghome {
|
||||
border: 1px solid black;
|
||||
cursor: move;
|
||||
background-color: #B0C4DE;
|
||||
display:inline-block;
|
||||
height: auto;
|
||||
width: auto;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.que.ddimageortext .group1, form.mform fieldset#id_previewareaheader .group1 {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
.que.ddimageortext .group2, form.mform fieldset#id_previewareaheader .group2 {
|
||||
background-color: #B0C4DE;
|
||||
}
|
||||
.que.ddimageortext .group3, form.mform fieldset#id_previewareaheader .group3 {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
.que.ddimageortext .group4, form.mform fieldset#id_previewareaheader .group4 {
|
||||
background-color: #D8BFD8;
|
||||
}
|
||||
.que.ddimageortext .group5, form.mform fieldset#id_previewareaheader .group5 {
|
||||
background-color: #87CEFA;
|
||||
}
|
||||
.que.ddimageortext .group6, form.mform fieldset#id_previewareaheader .group6 {
|
||||
background-color: #DAA520;
|
||||
}
|
||||
.que.ddimageortext .group7, form.mform fieldset#id_previewareaheader .group7 {
|
||||
background-color: #FFD700;
|
||||
}
|
||||
.que.ddimageortext .group8, form.mform fieldset#id_previewareaheader .group8 {
|
||||
background-color: #F0E68C;
|
||||
}
|
||||
.que.ddimageortext .drag, form.mform fieldset#id_previewareaheader .drag {
|
||||
border: 1px solid black;
|
||||
cursor: move;
|
||||
z-index: 2;
|
||||
}
|
||||
.que.ddimageortext .dragitems.readonly .drag {
|
||||
cursor: auto;
|
||||
}
|
||||
.que.ddimageortext div.ddarea, form.mform fieldset#id_previewareaheader div.ddarea {
|
||||
text-align : center;
|
||||
}
|
||||
.que.ddimageortext .dropbackground, form.mform fieldset#id_previewareaheader .dropbackground {
|
||||
margin:0 auto;
|
||||
}
|
||||
.que.ddimageortext .dropzone {
|
||||
border: 1px solid black;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.que.ddimageortext .dropzone.yui3-dd-drop-over.yui3-dd-drop-active-valid {
|
||||
border-color: #0a0;
|
||||
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
|
||||
}
|
||||
|
||||
.que.ddimageortext div.dragitems div.draghome, .que.ddimageortext div.dragitems div.drag,
|
||||
form.mform fieldset#id_previewareaheader div.draghome, form.mform fieldset#id_previewareaheader div.drag {
|
||||
font:13px/1.231 arial,helvetica,clean,sans-serif;
|
||||
}
|
||||
form.mform fieldset#id_previewareaheader div.drag.yui3-dd-dragging,
|
||||
.que.ddimageortext div.drag.yui3-dd-dragging {
|
||||
z-index: 3;
|
||||
box-shadow: 3px 3px 4px #000;
|
||||
}
|
||||
/* Editing form. Style repeated elements*/
|
||||
/*Top*/
|
||||
body#page-question-type-ddimageortext div[id^=fgroup_id_][id*=drags_] {
|
||||
background: #EEE;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid #BBB;
|
||||
border-bottom: 0;
|
||||
}
|
||||
body#page-question-type-ddimageortext div[id^=fgroup_id_][id*=drags_] .fgrouplabel label {
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Middle */
|
||||
body#page-question-type-ddimageortext div[id^=fitem_id_][id*=dragitem_] {
|
||||
background: #EEE;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid #BBB;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
/* Bottom */
|
||||
body#page-question-type-ddimageortext div[id^=fitem_id_][id*=draglabel_] {
|
||||
background: #EEE;
|
||||
margin-bottom: 2em;
|
||||
margin-top: 0;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid #BBB;
|
||||
border-top: 0;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
@qtype @qtype_ddimageortext
|
||||
Feature: Test creating a drag and drop onto image question
|
||||
As a teacher
|
||||
In order to test my students
|
||||
I need to be able to create drag and drop onto image questions
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a drag and drop onto image question
|
||||
When I press "Create a new question ..."
|
||||
And I set the field "Drag and drop onto image" to "1"
|
||||
And I press "Add"
|
||||
And I set the field "Question name" to "Drag and drop onto image 001"
|
||||
And I set the field "Question text" to "Identify the features in this cross-section."
|
||||
And I set the field "General feedback" to "The locations are now labelled on the diagram below."
|
||||
And I upload "question/type/ddimageortext/tests/fixtures/oceanfloorbase.jpg" file to "Background image" filemanager
|
||||
|
||||
# Draggable items
|
||||
And I follow "Draggable items"
|
||||
And I press "Blanks for 3 more draggable items"
|
||||
|
||||
And I set the field "id_drags_0_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_0" to "island<br/>arc"
|
||||
|
||||
And I set the field "id_drags_1_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_1" to "mid-ocean<br/>ridge"
|
||||
|
||||
And I set the field "id_drags_2_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_2" to "abyssal<br/>plain"
|
||||
|
||||
And I set the field "id_drags_3_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_3" to "continental<br/>rise"
|
||||
|
||||
And I set the field "id_drags_4_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_4" to "ocean<br/>trench"
|
||||
|
||||
And I set the field "id_drags_5_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_5" to "continental<br/>slope"
|
||||
|
||||
And I set the field "id_drags_6_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_6" to "mountain<br/>belt"
|
||||
|
||||
And I set the field "id_drags_7_dragitemtype" to "Draggable text"
|
||||
And I set the field "id_draglabel_7" to "continental<br/>shelf"
|
||||
|
||||
# Drop zones
|
||||
And I follow "Drop zones"
|
||||
And I press "Blanks for 3 more drop zones"
|
||||
|
||||
And I set the field "id_drops_0_xleft" to "53"
|
||||
And I set the field "id_drops_0_ytop" to "17"
|
||||
And I set the field "id_drops_0_choice" to "7"
|
||||
|
||||
And I set the field "id_drops_1_xleft" to "172"
|
||||
And I set the field "id_drops_1_ytop" to "2"
|
||||
And I set the field "id_drops_1_choice" to "8"
|
||||
|
||||
And I set the field "id_drops_2_xleft" to "363"
|
||||
And I set the field "id_drops_2_ytop" to "31"
|
||||
And I set the field "id_drops_2_choice" to "5"
|
||||
|
||||
And I set the field "id_drops_3_xleft" to "440"
|
||||
And I set the field "id_drops_3_ytop" to "13"
|
||||
And I set the field "id_drops_3_choice" to "3"
|
||||
|
||||
And I set the field "id_drops_4_xleft" to "115"
|
||||
And I set the field "id_drops_4_ytop" to "74"
|
||||
And I set the field "id_drops_4_choice" to "6"
|
||||
|
||||
And I set the field "id_drops_5_xleft" to "210"
|
||||
And I set the field "id_drops_5_ytop" to "94"
|
||||
And I set the field "id_drops_5_choice" to "4"
|
||||
|
||||
And I set the field "id_drops_6_xleft" to "310"
|
||||
And I set the field "id_drops_6_ytop" to "87"
|
||||
And I set the field "id_drops_6_choice" to "1"
|
||||
|
||||
And I set the field "id_drops_7_xleft" to "479"
|
||||
And I set the field "id_drops_7_ytop" to "84"
|
||||
And I set the field "id_drops_7_choice" to "2"
|
||||
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Drag and drop onto image 001"
|
||||
@@ -0,0 +1,101 @@
|
||||
@qtype @qtype_ddimageortext
|
||||
Feature: Test duplicating a quiz containing a drag and drop onto image question
|
||||
As a teacher
|
||||
In order re-use my courses containing drag and drop onto image questions
|
||||
I need to be able to backup and restore them
|
||||
|
||||
Background:
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddimageortext | Drag onto image | xsection |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And quiz "Test quiz" contains the following questions:
|
||||
| Drag onto image | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
|
||||
@javascript
|
||||
Scenario: Backup and restore a course containing a drag and drop onto image question
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
And I restore "test_backup.mbz" backup into a new course using this options:
|
||||
| Schema | Course name | Course 2 |
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
And I click on "Edit" "link" in the "Drag onto image" "table_row"
|
||||
Then the following fields match these values:
|
||||
| Question name | Drag onto image |
|
||||
| General feedback | <p>More information about the major features of the Earth's surface can be found in Block 3, Section 6.2.</p> |
|
||||
| Default mark | 1 |
|
||||
| Shuffle | 0 |
|
||||
| id_drags_0_dragitemtype | Draggable text |
|
||||
| id_drags_0_draggroup | 1 |
|
||||
| id_draglabel_0 | island<br/>arc |
|
||||
| id_drags_1_dragitemtype | Draggable text |
|
||||
| id_drags_1_draggroup | 1 |
|
||||
| id_draglabel_1 | mid-ocean<br/>ridge |
|
||||
| id_drags_2_dragitemtype | Draggable text |
|
||||
| id_drags_2_draggroup | 1 |
|
||||
| id_draglabel_2 | abyssal<br/>plain |
|
||||
| id_drags_3_dragitemtype | Draggable text |
|
||||
| id_drags_3_draggroup | 1 |
|
||||
| id_draglabel_3 | continental<br/>rise |
|
||||
| id_drags_4_dragitemtype | Draggable text |
|
||||
| id_drags_4_draggroup | 1 |
|
||||
| id_draglabel_4 | ocean<br/>trench |
|
||||
| id_drags_5_dragitemtype | Draggable text |
|
||||
| id_drags_5_draggroup | 1 |
|
||||
| id_draglabel_5 | continental<br/>slope |
|
||||
| id_drags_6_dragitemtype | Draggable text |
|
||||
| id_drags_6_draggroup | 1 |
|
||||
| id_draglabel_6 | mountain<br/>belt |
|
||||
| id_drags_7_dragitemtype | Draggable text |
|
||||
| id_drags_7_draggroup | 1 |
|
||||
| id_draglabel_7 | continental<br/>shelf |
|
||||
| id_drops_0_xleft | 53 |
|
||||
| id_drops_0_ytop | 17 |
|
||||
| id_drops_0_choice | 7. mountainbelt |
|
||||
| id_drops_1_xleft | 172 |
|
||||
| id_drops_1_ytop | 2 |
|
||||
| id_drops_1_choice | 8. continentalshelf |
|
||||
| id_drops_2_xleft | 363 |
|
||||
| id_drops_2_ytop | 31 |
|
||||
| id_drops_2_choice | 5. oceantrench |
|
||||
| id_drops_3_xleft | 440 |
|
||||
| id_drops_3_ytop | 13 |
|
||||
| id_drops_3_choice | 3. abyssalplain |
|
||||
| id_drops_4_xleft | 115 |
|
||||
| id_drops_4_ytop | 74 |
|
||||
| id_drops_4_choice | 6. continentalslope |
|
||||
| id_drops_5_xleft | 210 |
|
||||
| id_drops_5_ytop | 94 |
|
||||
| id_drops_5_choice | 4. continentalrise |
|
||||
| id_drops_6_xleft | 310 |
|
||||
| id_drops_6_ytop | 87 |
|
||||
| id_drops_6_choice | 1. islandarc |
|
||||
| id_drops_7_xleft | 479 |
|
||||
| id_drops_7_ytop | 84 |
|
||||
| id_drops_7_choice | 2. mid-oceanridge |
|
||||
| For any correct response | Well done! |
|
||||
| For any partially correct response | Parts, but only parts, of your response are correct. |
|
||||
| id_shownumcorrect | 1 |
|
||||
| For any incorrect response | That is not right at all. |
|
||||
| Penalty for each incorrect try | 0.3333333 |
|
||||
| Hint 1 | Incorrect placements will be removed. |
|
||||
| id_hintclearwrong_0 | 1 |
|
||||
| id_hintshownumcorrect_0 | 1 |
|
||||
| id_hintclearwrong_1 | 0 |
|
||||
| id_hintshownumcorrect_1 | 1 |
|
||||
| Hint 3 | Incorrect placements will be removed. |
|
||||
| id_hintclearwrong_2 | 1 |
|
||||
| id_hintshownumcorrect_2 | 1 |
|
||||
| id_hintclearwrong_3 | 0 |
|
||||
| id_hintshownumcorrect_3 | 1 |
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
// This file is part of Stack - http://stack.bham.ac.uk/
|
||||
//
|
||||
// Stack 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.
|
||||
//
|
||||
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Behat steps definitions for drag and drop onto image.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @category test
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
|
||||
|
||||
/**
|
||||
* Steps definitions related with the drag and drop onto image question type.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_qtype_ddimageortext extends behat_base {
|
||||
|
||||
/**
|
||||
* Get the xpath for a given drag item.
|
||||
* @param string $dragitem the text of the item to drag.
|
||||
* @return string the xpath expression.
|
||||
*/
|
||||
protected function drag_xpath($dragitem) {
|
||||
return '//div[contains(@class, " drag ") and contains(normalize-space(.), "' . $this->escape($dragitem) . '")]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the xpath for a given drop box.
|
||||
* @param string $placenumber the number of the drop box.
|
||||
* @return string the xpath expression.
|
||||
*/
|
||||
protected function drop_xpath($placenumber) {
|
||||
return '//div[contains(@class, "dropzone ") and contains(@class, "place' . $placenumber . ' ")]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag the drag item with the given text to the given space.
|
||||
*
|
||||
* @param string $dragitem the text of the item to drag.
|
||||
* @param int $placenumber the number of the place to drop into.
|
||||
*
|
||||
* @Given /^I drag "(?P<drag_item>[^"]*)" to place "(?P<place_number>\d+)" in the drag and drop onto image question$/
|
||||
*/
|
||||
public function i_drag_to_place_in_the_drag_and_drop_onto_image_question($dragitem, $placenumber) {
|
||||
$generalcontext = behat_context_helper::get('behat_general');
|
||||
$generalcontext->i_drag_and_i_drop_it_in($this->drag_xpath($dragitem),
|
||||
'xpath_element', $this->drop_xpath($placenumber), 'xpath_element');
|
||||
}
|
||||
|
||||
/**
|
||||
* Type some characters while focussed on a given drop box.
|
||||
*
|
||||
* @param string $keys the characters to type.
|
||||
* @param int $placenumber the number of the place to drop into.
|
||||
*
|
||||
* @Given /^I type "(?P<keys>[^"]*)" on place "(?P<place_number>\d+)" in the drag and drop onto image question$/
|
||||
*/
|
||||
public function i_type_on_place_in_the_drag_and_drop_onto_image_question($keys, $placenumber) {
|
||||
$node = $this->get_selected_node('xpath_element', $this->drop_xpath($placenumber));
|
||||
$this->ensure_node_is_visible($node);
|
||||
foreach (str_split($keys) as $key) {
|
||||
$node->keyDown($key);
|
||||
$node->keyPress($key);
|
||||
$node->keyUp($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@qtype @qtype_ddimageortext
|
||||
Feature: Test editing a drag and drop onto image questions
|
||||
As a teacher
|
||||
In order to be able to update my drag and drop onto image questions
|
||||
I need to edit them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddimageortext | Drag onto image | xsection |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Edit a drag and drop onto image question
|
||||
When I click on "Edit" "link" in the "Drag onto image" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Question name | Edited question name |
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Edited question name"
|
||||
@@ -0,0 +1,37 @@
|
||||
@qtype @qtype_ddimageortext
|
||||
Feature: Test exporting drag and drop onto image questions
|
||||
As a teacher
|
||||
In order to be able to reuse my drag and drop onto image questions
|
||||
I need to export them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddimageortext | Drag onto image | xsection |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
|
||||
@javascript
|
||||
Scenario: Export a drag and drop onto image question
|
||||
# Import sample file.
|
||||
When I navigate to "Export" node in "Course administration > Question bank"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I press "Export questions to file"
|
||||
And following "click here" should download between "18500" and "19000" bytes
|
||||
# If the download step is the last in the scenario then we can sometimes run
|
||||
# into the situation where the download page causes a http redirect but behat
|
||||
# has already conducted its reset (generating an error). By putting a logout
|
||||
# step we avoid behat doing the reset until we are off that page.
|
||||
And I log out
|
||||
@@ -0,0 +1,30 @@
|
||||
@qtype @qtype_ddimageortext
|
||||
Feature: Test importing drag and drop onto image questions
|
||||
As a teacher
|
||||
In order to reuse drag and drop onto image questions
|
||||
I need to import them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import drag and drop onto image question.
|
||||
When I navigate to "Import" node in "Course administration > Question bank"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I upload "question/type/ddimageortext/tests/fixtures/testquestion.moodle.xml" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Parsing questions from import file."
|
||||
And I should see "Importing 1 questions from file"
|
||||
And I should see "Identify the features in this cross-section by dragging the labels into the boxes."
|
||||
And I press "Continue"
|
||||
And I should see "Imported Drag and drop onto image 001"
|
||||
@@ -0,0 +1,60 @@
|
||||
@qtype @qtype_ddimageortext
|
||||
Feature: Preview a drag-drop onto image question
|
||||
As a teacher
|
||||
In order to check my drag-drop onto image questions will work for students
|
||||
I need to preview them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddimageortext | Drag onto image | xsection |
|
||||
Given I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a question using the mouse.
|
||||
When I click on "Preview" "link" in the "Drag onto image" "table_row"
|
||||
And I switch to "questionpreview" window
|
||||
# Odd, but the <br>s go to nothing, not a space.
|
||||
And I drag "mountainbelt" to place "1" in the drag and drop onto image question
|
||||
And I drag "continentalshelf" to place "2" in the drag and drop onto image question
|
||||
And I drag "oceantrench" to place "3" in the drag and drop onto image question
|
||||
And I drag "abyssalplain" to place "4" in the drag and drop onto image question
|
||||
And I drag "continentalslope" to place "5" in the drag and drop onto image question
|
||||
And I drag "continentalrise" to place "6" in the drag and drop onto image question
|
||||
And I drag "islandarc" to place "7" in the drag and drop onto image question
|
||||
And I drag "mid-oceanridge" to place "8" in the drag and drop onto image question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Identify the features" question is shown as "Correct"
|
||||
And I should see "Mark 1.00 out of 1.00"
|
||||
And I switch to the main window
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a question using the keyboard.
|
||||
When I click on "Preview" "link" in the "Drag onto image" "table_row"
|
||||
And I switch to "questionpreview" window
|
||||
And I type " " on place "1" in the drag and drop onto image question
|
||||
And I type " " on place "2" in the drag and drop onto image question
|
||||
And I type " " on place "3" in the drag and drop onto image question
|
||||
And I type " " on place "4" in the drag and drop onto image question
|
||||
And I type " " on place "5" in the drag and drop onto image question
|
||||
And I type " " on place "6" in the drag and drop onto image question
|
||||
And I type " " on place "7" in the drag and drop onto image question
|
||||
And I type " " on place "8" in the drag and drop onto image question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Identify the features" question is shown as "Correct"
|
||||
And I should see "Mark 1.00 out of 1.00"
|
||||
And I switch to the main window
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,251 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Test helpers for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Test helper class for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_test_helper extends question_test_helper {
|
||||
public function get_test_questions() {
|
||||
return array('fox', 'maths', 'xsection');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return qtype_ddimageortext_question
|
||||
*/
|
||||
public function make_ddimageortext_question_fox() {
|
||||
question_bank::load_question_definition_classes('ddimageortext');
|
||||
$dd = new qtype_ddimageortext_question();
|
||||
|
||||
test_question_maker::initialise_a_question($dd);
|
||||
|
||||
$dd->name = 'Drag-and-drop onto image question';
|
||||
$dd->questiontext = 'The quick brown fox jumped over the lazy dog.';
|
||||
$dd->generalfeedback = 'This sentence uses each letter of the alphabet.';
|
||||
$dd->qtype = question_bank::get_qtype('ddimageortext');
|
||||
|
||||
$dd->shufflechoices = true;
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_fields($dd);
|
||||
|
||||
$dd->choices = $this->make_choice_structure(array(
|
||||
new qtype_ddimageortext_drag_item('quick', 1, 1),
|
||||
new qtype_ddimageortext_drag_item('fox', 2, 1),
|
||||
new qtype_ddimageortext_drag_item('lazy', 3, 2),
|
||||
new qtype_ddimageortext_drag_item('dog', 4, 2)
|
||||
|
||||
));
|
||||
|
||||
$dd->places = $this->make_place_structure(array(
|
||||
new qtype_ddimageortext_drop_zone('', 1, 1),
|
||||
new qtype_ddimageortext_drop_zone('', 2, 1),
|
||||
new qtype_ddimageortext_drop_zone('', 3, 2),
|
||||
new qtype_ddimageortext_drop_zone('', 4, 2)
|
||||
));
|
||||
$dd->rightchoices = array(1 => 1, 2 => 2, 3 => 1, 4 => 4);
|
||||
|
||||
return $dd;
|
||||
}
|
||||
|
||||
protected function make_choice_structure($choices) {
|
||||
$choicestructure = array();
|
||||
foreach ($choices as $choice) {
|
||||
if (!isset($choicestructure[$choice->group])) {
|
||||
$choicestructure[$choice->group][1] = $choice;
|
||||
} else {
|
||||
$choicestructure[$choice->group][$choice->no] = $choice;
|
||||
}
|
||||
}
|
||||
return $choicestructure;
|
||||
}
|
||||
|
||||
protected function make_place_structure($places) {
|
||||
$placestructure = array();
|
||||
foreach ($places as $place) {
|
||||
$placestructure[$place->no] = $place;
|
||||
}
|
||||
return $placestructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a mathematical ddimageortext question.
|
||||
*
|
||||
* @return qtype_ddimageortext_question
|
||||
*/
|
||||
public function make_ddimageortext_question_maths() {
|
||||
question_bank::load_question_definition_classes('ddimageortext');
|
||||
$dd = new qtype_ddimageortext_question();
|
||||
|
||||
test_question_maker::initialise_a_question($dd);
|
||||
|
||||
$dd->name = 'Drag-and-drop onto image question';
|
||||
$dd->questiontext = 'Fill in the operators to make this equation work: ' .
|
||||
'7 [[1]] 11 [[2]] 13 [[1]] 17 [[2]] 19 = 3';
|
||||
$dd->generalfeedback = 'This sentence uses each letter of the alphabet.';
|
||||
$dd->qtype = question_bank::get_qtype('ddimageortext');
|
||||
|
||||
$dd->shufflechoices = true;
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_fields($dd);
|
||||
|
||||
$dd->choices = $this->make_choice_structure(array(
|
||||
new qtype_ddimageortext_drag_item('+', 1, 1),
|
||||
new qtype_ddimageortext_drag_item('-', 2, 1)
|
||||
));
|
||||
|
||||
$dd->places = $this->make_place_structure(array(
|
||||
new qtype_ddimageortext_drop_zone('', 1, 1),
|
||||
new qtype_ddimageortext_drop_zone('', 2, 1),
|
||||
new qtype_ddimageortext_drop_zone('', 3, 1),
|
||||
new qtype_ddimageortext_drop_zone('', 4, 1)
|
||||
));
|
||||
$dd->rightchoices = array(1 => 1, 2 => 2, 3 => 1, 4 => 2);
|
||||
|
||||
return $dd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass date to create a ddimageortext question.
|
||||
*/
|
||||
public function get_ddimageortext_question_form_data_xsection() {
|
||||
global $CFG, $USER;
|
||||
$fromform = new stdClass();
|
||||
|
||||
$bgdraftitemid = 0;
|
||||
file_prepare_draft_area($bgdraftitemid, null, null, null, null);
|
||||
$fs = get_file_storage();
|
||||
$filerecord = new stdClass();
|
||||
$filerecord->contextid = context_user::instance($USER->id)->id;
|
||||
$filerecord->component = 'user';
|
||||
$filerecord->filearea = 'draft';
|
||||
$filerecord->itemid = $bgdraftitemid;
|
||||
$filerecord->filepath = '/';
|
||||
$filerecord->filename = 'oceanfloorbase.jpg';
|
||||
$fs->create_file_from_pathname($filerecord, $CFG->dirroot .
|
||||
'/question/type/ddimageortext/tests/fixtures/oceanfloorbase.jpg');
|
||||
|
||||
$fromform->name = 'Geography cross-section';
|
||||
$fromform->questiontext = array(
|
||||
'text' => '<p>Identify the features in this cross-section by dragging the labels into the boxes.</p>
|
||||
<p><em>Use the mouse to drag the boxed words into the empty boxes. '.
|
||||
'Alternatively, use the tab key to select an empty box, '.
|
||||
'then use the space key to cycle through the options.</em></p>',
|
||||
'format' => FORMAT_HTML,
|
||||
);
|
||||
$fromform->defaultmark = 1;
|
||||
$fromform->generalfeedback = array(
|
||||
'text' => '<p>More information about the major features of the Earth\'s surface '.
|
||||
'can be found in Block 3, Section 6.2.</p>',
|
||||
'format' => FORMAT_HTML,
|
||||
);
|
||||
$fromform->bgimage = $bgdraftitemid;
|
||||
$fromform->shuffleanswers = 0;
|
||||
$fromform->drags = array(
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
array('dragitemtype' => 'word', 'draggroup' => '1', 'infinite' => '0'),
|
||||
);
|
||||
$fromform->dragitem = array(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
$fromform->draglabel =
|
||||
array(
|
||||
'island<br/>arc',
|
||||
'mid-ocean<br/>ridge',
|
||||
'abyssal<br/>plain',
|
||||
'continental<br/>rise',
|
||||
'ocean<br/>trench',
|
||||
'continental<br/>slope',
|
||||
'mountain<br/>belt',
|
||||
'continental<br/>shelf',
|
||||
);
|
||||
$fromform->drops = array(
|
||||
array('xleft' => '53', 'ytop' => '17', 'choice' => '7', 'droplabel' => ''),
|
||||
array('xleft' => '172', 'ytop' => '2', 'choice' => '8', 'droplabel' => ''),
|
||||
array('xleft' => '363', 'ytop' => '31', 'choice' => '5', 'droplabel' => ''),
|
||||
array('xleft' => '440', 'ytop' => '13', 'choice' => '3', 'droplabel' => ''),
|
||||
array('xleft' => '115', 'ytop' => '74', 'choice' => '6', 'droplabel' => ''),
|
||||
array('xleft' => '210', 'ytop' => '94', 'choice' => '4', 'droplabel' => ''),
|
||||
array('xleft' => '310', 'ytop' => '87', 'choice' => '1', 'droplabel' => ''),
|
||||
array('xleft' => '479', 'ytop' => '84', 'choice' => '2', 'droplabel' => ''),
|
||||
);
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_form_data($fromform);
|
||||
|
||||
$fromform->penalty = '0.3333333';
|
||||
$fromform->hint = array(
|
||||
array(
|
||||
'text' => '<p>Incorrect placements will be removed.</p>',
|
||||
'format' => FORMAT_HTML,
|
||||
),
|
||||
array(
|
||||
'text' => '<ul>
|
||||
<li>The abyssal plain is a flat almost featureless expanse of ocean '.
|
||||
'floor 4km to 6km below sea-level.</li>
|
||||
<li>The continental rise is the gently sloping part of the ocean floor beyond the continental slope.</li>
|
||||
<li>The continental shelf is the gently sloping ocean floor just offshore from the land.</li>
|
||||
<li>The continental slope is the relatively steep part of the ocean floor '.
|
||||
'beyond the continental shelf.</li>
|
||||
<li>A mid-ocean ridge is a broad submarine ridge several kilometres high.</li>
|
||||
<li>A mountain belt is a long range of mountains.</li>
|
||||
<li>An island arc is a chain of volcanic islands.</li>
|
||||
<li>An oceanic trench is a deep trough in the ocean floor.</li>
|
||||
</ul>',
|
||||
'format' => FORMAT_HTML,
|
||||
),
|
||||
array(
|
||||
'text' => '<p>Incorrect placements will be removed.</p>',
|
||||
'format' => FORMAT_HTML,
|
||||
),
|
||||
array(
|
||||
'text' => '<ul>
|
||||
<li>The abyssal plain is a flat almost featureless expanse of ocean '.
|
||||
'floor 4km to 6km below sea-level.</li>
|
||||
<li>The continental rise is the gently sloping part of the ocean floor beyond the continental slope.</li>
|
||||
<li>The continental shelf is the gently sloping ocean floor just offshore from the land.</li>
|
||||
<li>The continental slope is the relatively steep part of the ocean floor '.
|
||||
'beyond the continental shelf.</li>
|
||||
<li>A mid-ocean ridge is a broad submarine ridge several kilometres high.</li>
|
||||
<li>A mountain belt is a long range of mountains.</li>
|
||||
<li>An island arc is a chain of volcanic islands.</li>
|
||||
<li>An oceanic trench is a deep trough in the ocean floor.</li>
|
||||
</ul>',
|
||||
'format' => FORMAT_HTML,
|
||||
),
|
||||
);
|
||||
$fromform->hintclearwrong = array(1, 0, 1, 0);
|
||||
$fromform->hintshownumcorrect = array(1, 1, 1, 1);
|
||||
|
||||
return $fromform;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop onto image question definition class.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the matching question definition class.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_question_test extends basic_testcase {
|
||||
|
||||
public function test_get_question_summary() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$this->assertEquals('The quick brown fox jumped over the lazy dog.; '.
|
||||
'[[Drop zone 1]] -> {1. quick / 2. fox}; '.
|
||||
'[[Drop zone 2]] -> {1. quick / 2. fox}; '.
|
||||
'[[Drop zone 3]] -> {3. lazy / 4. dog}; '.
|
||||
'[[Drop zone 4]] -> {3. lazy / 4. dog}',
|
||||
$dd->get_question_summary());
|
||||
}
|
||||
|
||||
public function test_get_question_summary_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$this->assertEquals('Fill in the operators to make this equation work: '.
|
||||
'7 [[1]] 11 [[2]] 13 [[1]] 17 [[2]] 19 = 3; '.
|
||||
'[[Drop zone 1]] -> {1. + / 2. -}; '.
|
||||
'[[Drop zone 2]] -> {1. + / 2. -}; '.
|
||||
'[[Drop zone 3]] -> {1. + / 2. -}; '.
|
||||
'[[Drop zone 4]] -> {1. + / 2. -}',
|
||||
$dd->get_question_summary());
|
||||
}
|
||||
|
||||
public function test_summarise_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals('Drop zone 1 -> {1. quick} '.
|
||||
'Drop zone 2 -> {1. quick} '.
|
||||
'Drop zone 3 -> {3. lazy} '.
|
||||
'Drop zone 4 -> {3. lazy}',
|
||||
$dd->summarise_response(array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
|
||||
}
|
||||
|
||||
public function test_summarise_response_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals('Drop zone 1 -> {1. +} '.
|
||||
'Drop zone 2 -> {2. -} '.
|
||||
'Drop zone 3 -> {1. +} '.
|
||||
'Drop zone 4 -> {2. -}',
|
||||
$dd->summarise_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
|
||||
}
|
||||
|
||||
public function test_get_random_guess_score() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$this->assertEquals(0.5, $dd->get_random_guess_score());
|
||||
}
|
||||
|
||||
public function test_get_random_guess_score_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$this->assertEquals(0.5, $dd->get_random_guess_score());
|
||||
}
|
||||
|
||||
public function test_get_right_choice_for() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(1));
|
||||
$this->assertEquals(2, $dd->get_right_choice_for(2));
|
||||
}
|
||||
|
||||
public function test_get_right_choice_for_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(1));
|
||||
$this->assertEquals(2, $dd->get_right_choice_for(2));
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(3));
|
||||
$this->assertEquals(2, $dd->get_right_choice_for(4));
|
||||
}
|
||||
|
||||
public function test_clear_wrong_from_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$initialresponse = array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1');
|
||||
$this->assertEquals(array('p1' => '1', 'p2' => '', 'p3' => '1', 'p4' => ''),
|
||||
$dd->clear_wrong_from_response($initialresponse));
|
||||
}
|
||||
|
||||
public function test_get_num_parts_right() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(2, 4),
|
||||
$dd->get_num_parts_right(array('p1' => '1', 'p2' => '1', 'p3' => '2', 'p4' => '2')));
|
||||
$this->assertEquals(array(4, 4),
|
||||
$dd->get_num_parts_right(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
|
||||
}
|
||||
|
||||
public function test_get_num_parts_right_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(2, 4),
|
||||
$dd->get_num_parts_right(array(
|
||||
'p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
|
||||
}
|
||||
|
||||
public function test_get_expected_data() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(
|
||||
array('p1' => PARAM_INT, 'p2' => PARAM_INT, 'p3' => PARAM_INT, 'p4' => PARAM_INT),
|
||||
$dd->get_expected_data()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_get_correct_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'),
|
||||
$dd->get_correct_response());
|
||||
}
|
||||
|
||||
public function test_get_correct_response_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'),
|
||||
$dd->get_correct_response());
|
||||
}
|
||||
|
||||
public function test_is_same_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertTrue($dd->is_same_response(
|
||||
array(),
|
||||
array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => '')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array(),
|
||||
array('p1' => '1', 'p2' => '', 'p3' => '', 'p4' => '')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => ''),
|
||||
array('p1' => '1', 'p2' => '', 'p3' => '', 'p4' => '')));
|
||||
|
||||
$this->assertTrue($dd->is_same_response(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '3', 'p4' => '4'),
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '3', 'p4' => '4')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '3', 'p4' => '4'),
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '2', 'p4' => '4')));
|
||||
}
|
||||
public function test_is_complete_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertFalse($dd->is_complete_response(array()));
|
||||
$this->assertFalse($dd->is_complete_response(
|
||||
array('p1' => '1', 'p2' => '1', 'p3' => '', 'p4' => '')));
|
||||
$this->assertFalse($dd->is_complete_response(array('p1' => '1')));
|
||||
$this->assertTrue($dd->is_complete_response(
|
||||
array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
|
||||
}
|
||||
|
||||
public function test_is_gradable_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertFalse($dd->is_gradable_response(array()));
|
||||
$this->assertFalse($dd->is_gradable_response(
|
||||
array('p1' => '', 'p2' => '', 'p3' => '', 'p3' => '')));
|
||||
$this->assertTrue($dd->is_gradable_response(
|
||||
array('p1' => '1', 'p2' => '1', 'p3' => '')));
|
||||
$this->assertTrue($dd->is_gradable_response(array('p1' => '1')));
|
||||
$this->assertTrue($dd->is_gradable_response(
|
||||
array('p1' => '1', 'p2' => '1', 'p3' => '1')));
|
||||
}
|
||||
|
||||
public function test_grading() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(1, question_state::$gradedright),
|
||||
$dd->grade_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
|
||||
$this->assertEquals(array(0.25, question_state::$gradedpartial),
|
||||
$dd->grade_response(array('p1' => '1')));
|
||||
$this->assertEquals(array(0, question_state::$gradedwrong),
|
||||
$dd->grade_response(array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1')));
|
||||
}
|
||||
|
||||
public function test_grading_maths() {
|
||||
$dd = test_question_maker::make_question('ddimageortext', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(1, question_state::$gradedright),
|
||||
$dd->grade_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
|
||||
$this->assertEquals(array(0.5, question_state::$gradedpartial),
|
||||
$dd->grade_response(array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
|
||||
$this->assertEquals(array(0, question_state::$gradedwrong),
|
||||
$dd->grade_response(array('p1' => '', 'p2' => '1', 'p3' => '2', 'p4' => '1')));
|
||||
}
|
||||
|
||||
public function test_classify_response() {
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(
|
||||
1 => new question_classified_response(1, '1. quick', 1),
|
||||
2 => new question_classified_response(2, '2. fox', 1),
|
||||
3 => new question_classified_response(3, '3. lazy', 1),
|
||||
4 => new question_classified_response(4, '4. dog', 1)
|
||||
), $dd->classify_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
|
||||
$this->assertEquals(array(
|
||||
1 => question_classified_response::no_response(),
|
||||
2 => new question_classified_response(1, '1. quick', 0),
|
||||
3 => new question_classified_response(4, '4. dog', 0),
|
||||
4 => new question_classified_response(4, '4. dog', 1)
|
||||
), $dd->classify_response(array('p1' => '', 'p2' => '1', 'p3' => '2', 'p4' => '2')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop onto image question definition class.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop onto image question definition class.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_test extends basic_testcase {
|
||||
/** @var qtype_ddimageortext instance of the question type class to test. */
|
||||
protected $qtype;
|
||||
|
||||
protected function setUp() {
|
||||
$this->qtype = question_bank::get_qtype('ddimageortext');;
|
||||
}
|
||||
|
||||
protected function tearDown() {
|
||||
$this->qtype = null;
|
||||
}
|
||||
|
||||
public function test_name() {
|
||||
$this->assertEquals($this->qtype->name(), 'ddimageortext');
|
||||
}
|
||||
|
||||
public function test_can_analyse_responses() {
|
||||
$this->assertTrue($this->qtype->can_analyse_responses());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,858 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddimageortext_walkthrough_test extends qbehaviour_walkthrough_test_base {
|
||||
|
||||
/**
|
||||
* Get an expectation that the output contains an item ready to drag.
|
||||
* @param int $dragitemno the item number.
|
||||
* @param int $choice which choice this is.
|
||||
* @param int $group which drag group it belongs to.
|
||||
* @return question_contains_tag_with_attributes the required expectation.
|
||||
*/
|
||||
protected function get_contains_drag_image_home_expectation($dragitemno, $choice, $group) {
|
||||
$class = 'group' . $group;
|
||||
$class .= ' draghome dragitemhomes' . $dragitemno. ' choice'.$choice.' yui3-cssfonts';
|
||||
|
||||
$expectedattrs = array();
|
||||
$expectedattrs['class'] = $class;
|
||||
|
||||
return new question_contains_tag_with_attributes('div', $expectedattrs);
|
||||
}
|
||||
|
||||
public function test_interactive_behaviour() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->hints = array(
|
||||
new question_hint_with_parts(13, 'This is the first hint.', FORMAT_HTML, false, false),
|
||||
new question_hint_with_parts(14, 'This is the second hint.', FORMAT_HTML, true, true),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the wrong answer.
|
||||
$this->process_submission(array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1'));
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 1),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
// Submit the wrong answer.
|
||||
$this->process_submission(
|
||||
array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 1),
|
||||
$this->get_contains_try_again_button_expectation(true),
|
||||
$this->get_contains_hint_expectation('This is the first hint'));
|
||||
|
||||
// Do try again.
|
||||
$this->process_submission(array('-tryagain' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', '2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', '1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', '2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', '1'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(2),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit the right answer.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(8);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', '1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', '2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', '1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', '2'),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_correct_expectation(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(8);
|
||||
}
|
||||
|
||||
public function test_deferred_feedback() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Save a partial answer.
|
||||
$this->process_submission(array('p1' => '2', 'p2' => '1'));
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$invalid);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', ''),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', ''),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
// Save the right answer.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(12);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
|
||||
$this->get_contains_correct_expectation());
|
||||
|
||||
// Change the right answer a bit.
|
||||
$dd->rightchoices[2] = 1;
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(9);
|
||||
}
|
||||
|
||||
public function test_deferred_feedback_unanswered() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
$this->check_step_count(1);
|
||||
|
||||
// Save a blank response.
|
||||
$this->process_submission(array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => ''));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', ''),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', ''),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', ''),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', ''),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
$this->check_step_count(1);
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gaveup);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2));
|
||||
}
|
||||
|
||||
public function test_deferred_feedback_partial_answer() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Save a partial response.
|
||||
$this->process_submission(array('p1' => '1', 'p2' => '2', 'p3' => '', 'p4' => ''));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$invalid);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 0),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(1.5);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_partcorrect_expectation());
|
||||
}
|
||||
|
||||
public function test_interactive_grading() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->hints = array(
|
||||
new question_hint_with_parts(1, 'This is the first hint.',
|
||||
FORMAT_MOODLE, true, true),
|
||||
new question_hint_with_parts(2, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->assertEquals('interactivecountback',
|
||||
$this->quba->get_question_attempt($this->slot)->get_behaviour_name());
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_does_not_contain_num_parts_correct(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit an response with the first two parts right.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '2', 'p4' => '1', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_try_again_button_expectation(true),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_contains_hint_expectation('This is the first hint'),
|
||||
$this->get_contains_num_parts_correct(2),
|
||||
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 1));
|
||||
|
||||
// Check that extract responses will return the reset data.
|
||||
$prefix = $this->quba->get_field_prefix($this->slot);
|
||||
$this->assertEquals(array('p1' => '1', 'p2' => '2'),
|
||||
$this->quba->extract_responses($this->slot,
|
||||
array($prefix . 'p1' => '1', $prefix . 'p2' => '2', '-tryagain' => 1)));
|
||||
|
||||
// Do try again.
|
||||
// keys p3 and p4 are extra hidden fields to clear data.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '', 'p4' => '', '-tryagain' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 0),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_try_again_button_expectation(),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(2),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit an response with the first and last parts right.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '1', 'p3' => '2', 'p4' => '2', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_try_again_button_expectation(true),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_contains_hint_expectation('This is the second hint'),
|
||||
$this->get_contains_num_parts_correct(2),
|
||||
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 2));
|
||||
|
||||
// Do try again.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '', 'p3' => '', 'p4' => '2', '-tryagain' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_try_again_button_expectation(),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(1),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit the right answer.
|
||||
$this->process_submission(
|
||||
array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(7);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 1),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 2),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_does_not_contain_try_again_button_expectation(),
|
||||
$this->get_contains_correct_expectation(),
|
||||
$this->get_no_hint_visible_expectation(),
|
||||
$this->get_does_not_contain_num_parts_correct(),
|
||||
$this->get_contains_standard_correct_combined_feedback_expectation());
|
||||
}
|
||||
|
||||
public function test_interactive_correct_no_submit() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->hints = array(
|
||||
new question_hint_with_parts(23, 'This is the first hint.',
|
||||
FORMAT_MOODLE, false, false),
|
||||
new question_hint_with_parts(24, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the right answer.
|
||||
$this->process_submission(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'));
|
||||
|
||||
// Finish the attempt without clicking check.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_correct_expectation(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
}
|
||||
|
||||
public function test_interactive_partial_no_submit() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->hints = array(
|
||||
new question_hint_with_parts(23, 'This is the first hint.',
|
||||
FORMAT_MOODLE, false, false),
|
||||
new question_hint_with_parts(24, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 4);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the a partially right answer.
|
||||
$this->process_submission(array('p1' => '1', 'p2' => '1', 'p3' => '2', 'p4' => '1'));
|
||||
|
||||
// Finish the attempt without clicking check.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(1);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_partcorrect_expectation(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(1);
|
||||
}
|
||||
|
||||
public function test_interactive_no_right_clears() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$dd->hints = array(
|
||||
new question_hint_with_parts(23, 'This is the first hint.', FORMAT_MOODLE, false, true),
|
||||
new question_hint_with_parts(24, 'This is the second hint.', FORMAT_MOODLE, true, true),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_marked_out_of_summary(),
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the a completely wrong answer.
|
||||
$this->process_submission(
|
||||
array('p1' => '2', 'p2' => '1', 'p3' => '2', 'p4' => '1', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_marked_out_of_summary(),
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_hint_expectation('This is the first hint'));
|
||||
|
||||
// Do try again.
|
||||
$this->process_submission(
|
||||
array('p1' => '', 'p2' => '', 'p3' => '', 'p4' => '', '-tryagain' => 1));
|
||||
|
||||
// Check that all the wrong answers have been cleared.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_marked_out_of_summary(),
|
||||
$this->get_contains_drag_image_home_expectation(1, 1, 1),
|
||||
$this->get_contains_drag_image_home_expectation(2, 2, 1),
|
||||
$this->get_contains_drag_image_home_expectation(3, 1, 2),
|
||||
$this->get_contains_drag_image_home_expectation(4, 2, 2),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3', 0),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4', 0),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(2),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
}
|
||||
|
||||
public function test_display_of_right_answer_when_shuffled() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddimageortext');
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3'),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4'),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Save a partial answer.
|
||||
$this->process_submission($dd->get_correct_response());
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p1',
|
||||
$dd->get_right_choice_for(1)),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p2',
|
||||
$dd->get_right_choice_for(2)),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p3',
|
||||
$dd->get_right_choice_for(3)),
|
||||
$this->get_contains_hidden_expectation(
|
||||
$this->quba->get_field_prefix($this->slot) . 'p4',
|
||||
$dd->get_right_choice_for(4)),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->displayoptions->rightanswer = question_display_options::VISIBLE;
|
||||
$this->assertEquals('Drop zone 1 -> {1. quick} '.
|
||||
'Drop zone 2 -> {2. fox} '.
|
||||
'Drop zone 3 -> {3. lazy} '.
|
||||
'Drop zone 4 -> {4. dog}',
|
||||
$dd->get_right_answer_summary());
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Version information for the drag-and-drop onto image question type.
|
||||
*
|
||||
* @package qtype_ddimageortext
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2015091100;
|
||||
$plugin->requires = 2015050500;
|
||||
|
||||
$plugin->component = 'qtype_ddimageortext';
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
$plugin->dependencies = array(
|
||||
'qtype_gapselect' => 2015091100,
|
||||
);
|
||||
@@ -0,0 +1,524 @@
|
||||
YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
|
||||
var DDIMAGEORTEXT_DD = function() {
|
||||
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
|
||||
doc : null,
|
||||
polltimer : null,
|
||||
afterimageloaddone : false,
|
||||
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
|
||||
//in 'some' loop returning true breaks the loop and is passed as return value from
|
||||
//'some' else returns false. Can be though of as equivalent to ||.
|
||||
if (dragitemhome.get('tagName') !== 'IMG'){
|
||||
return false;
|
||||
}
|
||||
var done = (dragitemhome.get('complete'));
|
||||
if (waitforimageconstrain) {
|
||||
done = done && dragitemhome.hasClass('constrained');
|
||||
}
|
||||
return !done;
|
||||
});
|
||||
if (bgdone && alldragsloaded) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure : function (mainobj) {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node : function() {
|
||||
return topnode;
|
||||
},
|
||||
drag_items : function() {
|
||||
return dragitemsarea.all('.drag');
|
||||
},
|
||||
drop_zones : function() {
|
||||
return topnode.all('div.dropzones div.dropzone');
|
||||
},
|
||||
drop_zone_group : function(groupno) {
|
||||
return topnode.all('div.dropzones div.group' + groupno);
|
||||
},
|
||||
drag_items_cloned_from : function(dragitemno) {
|
||||
return dragitemsarea.all('.dragitems' + dragitemno);
|
||||
},
|
||||
drag_item : function(draginstanceno) {
|
||||
return dragitemsarea.one('.draginstance' + draginstanceno);
|
||||
},
|
||||
drag_items_in_group : function(groupno) {
|
||||
return dragitemsarea.all('.drag.group' + groupno);
|
||||
},
|
||||
drag_item_homes : function() {
|
||||
return dragitemsarea.all('.draghome');
|
||||
},
|
||||
bg_img : function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img : function (url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
|
||||
var oldhome = this.drag_item_home(dragitemno);
|
||||
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
|
||||
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
|
||||
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
|
||||
if (oldhome === null) {
|
||||
if (url) {
|
||||
dragitemsarea.append(imghtml);
|
||||
} else if (alt !== '') {
|
||||
dragitemsarea.append(divhtml);
|
||||
}
|
||||
} else {
|
||||
if (url) {
|
||||
dragitemsarea.insert(imghtml, oldhome);
|
||||
} else if (alt !== '') {
|
||||
dragitemsarea.insert(divhtml, oldhome);
|
||||
}
|
||||
oldhome.remove(true);
|
||||
}
|
||||
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
|
||||
if (newlycreated !== null) {
|
||||
newlycreated.setData('groupno', group);
|
||||
newlycreated.setData('dragitemno', dragitemno);
|
||||
}
|
||||
},
|
||||
drag_item_home : function (dragitemno) {
|
||||
return dragitemsarea.one('.dragitemhomes' + dragitemno);
|
||||
},
|
||||
get_classname_numeric_suffix : function(node, prefix) {
|
||||
var classes = node.getAttribute('class');
|
||||
if (classes !== '') {
|
||||
var classesarr = classes.split(' ');
|
||||
for (var index = 0; index < classesarr.length; index++) {
|
||||
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
|
||||
if (patt1.test(classesarr[index])) {
|
||||
var patt2 = new RegExp('([0-9])+$');
|
||||
var match = patt2.exec(classesarr[index]);
|
||||
return + match[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'Prefix "' + prefix + '" not found in class names.';
|
||||
},
|
||||
clone_new_drag_item : function (draginstanceno, dragitemno) {
|
||||
var draghome = this.drag_item_home(dragitemno);
|
||||
if (draghome === null) {
|
||||
return null;
|
||||
}
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('dragitemhomes' + dragitemno);
|
||||
drag.addClass('dragitems' + dragitemno);
|
||||
drag.addClass('draginstance' + draginstanceno);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('drag');
|
||||
drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
|
||||
drag.setData('draginstanceno', draginstanceno);
|
||||
drag.setData('dragitemno', dragitemno);
|
||||
draghome.get('parentNode').appendChild(drag);
|
||||
return drag;
|
||||
},
|
||||
draggable_for_question : function (drag, group, choice) {
|
||||
new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'point',
|
||||
groups: [group]
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
|
||||
|
||||
drag.setData('group', group);
|
||||
drag.setData('choice', choice);
|
||||
},
|
||||
draggable_for_form : function (drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'point'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
|
||||
dd.on('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
var draginstanceno = dragnode.getData('draginstanceno');
|
||||
var gooddrop = dragnode.getData('gooddrop');
|
||||
|
||||
if (!gooddrop) {
|
||||
mainobj.reset_drag_xy(draginstanceno);
|
||||
} else {
|
||||
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
|
||||
}
|
||||
}, this);
|
||||
dd.on('drag:start', function(e) {
|
||||
var drag = e.target;
|
||||
drag.get('node').setData('gooddrop', false);
|
||||
}, this);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
update_padding_sizes_all : function () {
|
||||
for (var groupno = 1; groupno <= 8; groupno++) {
|
||||
this.update_padding_size_for_group(groupno);
|
||||
}
|
||||
},
|
||||
update_padding_size_for_group : function (groupno) {
|
||||
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
|
||||
if (groupitems.size() !== 0) {
|
||||
var maxwidth = 0;
|
||||
var maxheight = 0;
|
||||
groupitems.each(function(item){
|
||||
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
|
||||
maxheight = Math.max(maxheight, item.get('clientHeight'));
|
||||
}, this);
|
||||
groupitems.each(function(item) {
|
||||
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
|
||||
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
|
||||
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
|
||||
margintopbottom + 'px ' + marginleftright + 'px');
|
||||
}, this);
|
||||
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
|
||||
'height': maxheight + 10});
|
||||
}
|
||||
},
|
||||
convert_to_window_xy : function (bgimgxy) {
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
}
|
||||
}, {
|
||||
NAME : DDIMAGEORTEXTDDNAME,
|
||||
ATTRS : {
|
||||
drops : {value : null},
|
||||
readonly : {value : false},
|
||||
topnode : {value : null}
|
||||
}
|
||||
});
|
||||
|
||||
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
|
||||
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
|
||||
|
||||
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
|
||||
var DDIMAGEORTEXT_QUESTION = function() {
|
||||
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
|
||||
touchscrolldisable: null,
|
||||
pendingid: '',
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.create_all_drag_and_drops);
|
||||
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.create_all_drag_and_drops);
|
||||
Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
|
||||
},
|
||||
|
||||
/**
|
||||
* prevent_touchmove_from_scrolling allows users of touch screen devices to
|
||||
* use drag and drop and normal scrolling at the same time. I.e. when
|
||||
* touching and dragging a draggable item, the screen does not scroll, but
|
||||
* you can scroll by touching other area of the screen apart from the
|
||||
* draggable items.
|
||||
*/
|
||||
prevent_touchmove_from_scrolling : function(drag) {
|
||||
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
|
||||
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
|
||||
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
|
||||
|
||||
// Disable scrolling when touching the draggable items.
|
||||
drag.on(touchstart, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
return; // Already disabled.
|
||||
}
|
||||
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
});
|
||||
}, this);
|
||||
|
||||
// Allow scrolling after releasing the draggable items.
|
||||
drag.on(touchend, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
this.touchscrolldisable.detach();
|
||||
this.touchscrolldisable = null;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
create_all_drag_and_drops : function () {
|
||||
this.init_drops();
|
||||
this.update_padding_sizes_all();
|
||||
var i = 0;
|
||||
this.doc.drag_item_homes().each(function(dragitemhome){
|
||||
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
|
||||
var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
|
||||
var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
|
||||
var groupsize = this.doc.drop_zone_group(group).size();
|
||||
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
|
||||
i++;
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.draggable_for_question(dragnode, group, choice);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(dragnode);
|
||||
}
|
||||
if (dragnode.hasClass('infinite')) {
|
||||
var dragstocreate = groupsize - 1;
|
||||
while (dragstocreate > 0) {
|
||||
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
|
||||
i++;
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.draggable_for_question(dragnode, group, choice);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(dragnode);
|
||||
}
|
||||
dragstocreate--;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.reposition_drags_for_question();
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.drop_zones().set('tabIndex', 0);
|
||||
this.doc.drop_zones().each(
|
||||
function(v){
|
||||
v.on('dragchange', this.drop_zone_key_press, this);
|
||||
}, this);
|
||||
}
|
||||
M.util.js_complete(this.pendingid);
|
||||
},
|
||||
drop_zone_key_press : function (e) {
|
||||
switch (e.direction) {
|
||||
case 'next' :
|
||||
this.place_next_drag_in(e.target);
|
||||
break;
|
||||
case 'previous' :
|
||||
this.place_previous_drag_in(e.target);
|
||||
break;
|
||||
case 'remove' :
|
||||
this.remove_drag_from_drop(e.target);
|
||||
break;
|
||||
}
|
||||
e.preventDefault();
|
||||
this.reposition_drags_for_question();
|
||||
},
|
||||
place_next_drag_in : function (drop) {
|
||||
this.search_for_unplaced_drop_choice(drop, 1);
|
||||
},
|
||||
place_previous_drag_in : function (drop) {
|
||||
this.search_for_unplaced_drop_choice(drop, -1);
|
||||
},
|
||||
search_for_unplaced_drop_choice : function (drop, direction) {
|
||||
var next;
|
||||
var current = this.current_drag_in_drop(drop);
|
||||
if ('' === current) {
|
||||
if (direction === 1) {
|
||||
next = 1;
|
||||
} else {
|
||||
next = 1;
|
||||
var groupno = drop.getData('group');
|
||||
this.doc.drag_items_in_group(groupno).each(function(drag) {
|
||||
next = Math.max(next, drag.getData('choice'));
|
||||
}, this);
|
||||
}
|
||||
} else {
|
||||
next = + current + direction;
|
||||
}
|
||||
var drag;
|
||||
do {
|
||||
if (this.get_choices_for_drop(next, drop).size() === 0){
|
||||
this.remove_drag_from_drop(drop);
|
||||
return;
|
||||
} else {
|
||||
drag = this.get_unplaced_choice_for_drop(next, drop);
|
||||
}
|
||||
next = next + direction;
|
||||
} while (drag === null);
|
||||
this.place_drag_in_drop(drag, drop);
|
||||
},
|
||||
current_drag_in_drop : function (drop) {
|
||||
var inputid = drop.getData('inputid');
|
||||
var inputnode = Y.one('input#' + inputid);
|
||||
return inputnode.get('value');
|
||||
},
|
||||
remove_drag_from_drop : function (drop) {
|
||||
this.place_drag_in_drop(null, drop);
|
||||
},
|
||||
place_drag_in_drop : function (drag, drop) {
|
||||
var inputid = drop.getData('inputid');
|
||||
var inputnode = Y.one('input#' + inputid);
|
||||
if (drag !== null) {
|
||||
inputnode.set('value', drag.getData('choice'));
|
||||
} else {
|
||||
inputnode.set('value', '');
|
||||
}
|
||||
},
|
||||
reposition_drags_for_question : function() {
|
||||
this.doc.drag_items().removeClass('placed');
|
||||
this.doc.drag_items().each (function (dragitem) {
|
||||
if (dragitem.dd !== undefined) {
|
||||
dragitem.dd.detachAll('drag:start');
|
||||
}
|
||||
}, this);
|
||||
this.doc.drop_zones().each(function(dropzone) {
|
||||
var relativexy = dropzone.getData('xy');
|
||||
dropzone.setXY(this.convert_to_window_xy(relativexy));
|
||||
var inputcss = 'input#' + dropzone.getData('inputid');
|
||||
var input = this.doc.top_node().one(inputcss);
|
||||
var choice = input.get('value');
|
||||
if (choice !== "") {
|
||||
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
|
||||
if (dragitem !== null) {
|
||||
dragitem.setXY(dropzone.getXY());
|
||||
dragitem.addClass('placed');
|
||||
if (dragitem.dd !== undefined) {
|
||||
dragitem.dd.once('drag:start', function (e, input) {
|
||||
input.set('value', '');
|
||||
e.target.get('node').removeClass('placed');
|
||||
},this, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(dragitem) {
|
||||
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
|
||||
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
|
||||
dragitem.setXY(dragitemhome.getXY());
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
get_choices_for_drop : function(choice, drop) {
|
||||
var group = drop.getData('group');
|
||||
return this.doc.top_node().all(
|
||||
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
|
||||
},
|
||||
get_unplaced_choice_for_drop : function(choice, drop) {
|
||||
var dragitems = this.get_choices_for_drop(choice, drop);
|
||||
var dragitem = null;
|
||||
dragitems.some(function (d) {
|
||||
if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
|
||||
dragitem = d;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return dragitem;
|
||||
},
|
||||
init_drops : function () {
|
||||
var dropareas = this.doc.top_node().one('div.dropzones');
|
||||
var groupnodes = {};
|
||||
for (var groupno = 1; groupno <= 8; groupno++) {
|
||||
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
|
||||
dropareas.append(groupnode);
|
||||
groupnodes[groupno] = groupnode;
|
||||
}
|
||||
var drop_hit_handler = function(e) {
|
||||
var drag = e.drag.get('node');
|
||||
var drop = e.drop.get('node');
|
||||
if (Number(drop.getData('group')) === drag.getData('group')){
|
||||
this.place_drag_in_drop(drag, drop);
|
||||
}
|
||||
};
|
||||
for (var dropno in this.get('drops')) {
|
||||
var drop = this.get('drops')[dropno];
|
||||
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
|
||||
var title = drop.text.replace('"', '\"');
|
||||
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '"> </div>';
|
||||
var dropnode = Y.Node.create(dropnodehtml);
|
||||
groupnodes[drop.group].append(dropnode);
|
||||
dropnode.setStyles({'opacity': 0.5});
|
||||
dropnode.setData('xy', drop.xy);
|
||||
dropnode.setData('place', dropno);
|
||||
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
|
||||
dropnode.setData('group', drop.group);
|
||||
var dropdd = new Y.DD.Drop({
|
||||
node: dropnode, groups : [drop.group]});
|
||||
dropdd.on('drop:hit', drop_hit_handler, this);
|
||||
}
|
||||
}
|
||||
}, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
|
||||
|
||||
Y.Event.define('dragchange', {
|
||||
// Webkit and IE repeat keydown when you hold down arrow keys.
|
||||
// Opera links keypress to page scroll; others keydown.
|
||||
// Firefox prevents page scroll via preventDefault() on either
|
||||
// keydown or keypress.
|
||||
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
|
||||
|
||||
_keys: {
|
||||
'32': 'next', // Space
|
||||
'37': 'previous', // Left arrow
|
||||
'38': 'previous', // Up arrow
|
||||
'39': 'next', // Right arrow
|
||||
'40': 'next', // Down arrow
|
||||
'27': 'remove' // Escape
|
||||
},
|
||||
|
||||
_keyHandler: function (e, notifier) {
|
||||
if (this._keys[e.keyCode]) {
|
||||
e.direction = this._keys[e.keyCode];
|
||||
notifier.fire(e);
|
||||
}
|
||||
},
|
||||
|
||||
on: function (node, sub, notifier) {
|
||||
sub._detacher = node.on(this._event, this._keyHandler,
|
||||
this, notifier);
|
||||
}
|
||||
});
|
||||
|
||||
M.qtype_ddimageortext.init_question = function(config) {
|
||||
return new DDIMAGEORTEXT_QUESTION(config);
|
||||
};
|
||||
|
||||
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});
|
||||
@@ -0,0 +1,524 @@
|
||||
YUI.add('moodle-qtype_ddimageortext-dd', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
|
||||
var DDIMAGEORTEXT_DD = function() {
|
||||
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
|
||||
doc : null,
|
||||
polltimer : null,
|
||||
afterimageloaddone : false,
|
||||
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
|
||||
//in 'some' loop returning true breaks the loop and is passed as return value from
|
||||
//'some' else returns false. Can be though of as equivalent to ||.
|
||||
if (dragitemhome.get('tagName') !== 'IMG'){
|
||||
return false;
|
||||
}
|
||||
var done = (dragitemhome.get('complete'));
|
||||
if (waitforimageconstrain) {
|
||||
done = done && dragitemhome.hasClass('constrained');
|
||||
}
|
||||
return !done;
|
||||
});
|
||||
if (bgdone && alldragsloaded) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure : function (mainobj) {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node : function() {
|
||||
return topnode;
|
||||
},
|
||||
drag_items : function() {
|
||||
return dragitemsarea.all('.drag');
|
||||
},
|
||||
drop_zones : function() {
|
||||
return topnode.all('div.dropzones div.dropzone');
|
||||
},
|
||||
drop_zone_group : function(groupno) {
|
||||
return topnode.all('div.dropzones div.group' + groupno);
|
||||
},
|
||||
drag_items_cloned_from : function(dragitemno) {
|
||||
return dragitemsarea.all('.dragitems' + dragitemno);
|
||||
},
|
||||
drag_item : function(draginstanceno) {
|
||||
return dragitemsarea.one('.draginstance' + draginstanceno);
|
||||
},
|
||||
drag_items_in_group : function(groupno) {
|
||||
return dragitemsarea.all('.drag.group' + groupno);
|
||||
},
|
||||
drag_item_homes : function() {
|
||||
return dragitemsarea.all('.draghome');
|
||||
},
|
||||
bg_img : function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img : function (url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
|
||||
var oldhome = this.drag_item_home(dragitemno);
|
||||
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
|
||||
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
|
||||
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
|
||||
if (oldhome === null) {
|
||||
if (url) {
|
||||
dragitemsarea.append(imghtml);
|
||||
} else if (alt !== '') {
|
||||
dragitemsarea.append(divhtml);
|
||||
}
|
||||
} else {
|
||||
if (url) {
|
||||
dragitemsarea.insert(imghtml, oldhome);
|
||||
} else if (alt !== '') {
|
||||
dragitemsarea.insert(divhtml, oldhome);
|
||||
}
|
||||
oldhome.remove(true);
|
||||
}
|
||||
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
|
||||
if (newlycreated !== null) {
|
||||
newlycreated.setData('groupno', group);
|
||||
newlycreated.setData('dragitemno', dragitemno);
|
||||
}
|
||||
},
|
||||
drag_item_home : function (dragitemno) {
|
||||
return dragitemsarea.one('.dragitemhomes' + dragitemno);
|
||||
},
|
||||
get_classname_numeric_suffix : function(node, prefix) {
|
||||
var classes = node.getAttribute('class');
|
||||
if (classes !== '') {
|
||||
var classesarr = classes.split(' ');
|
||||
for (var index = 0; index < classesarr.length; index++) {
|
||||
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
|
||||
if (patt1.test(classesarr[index])) {
|
||||
var patt2 = new RegExp('([0-9])+$');
|
||||
var match = patt2.exec(classesarr[index]);
|
||||
return + match[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'Prefix "' + prefix + '" not found in class names.';
|
||||
},
|
||||
clone_new_drag_item : function (draginstanceno, dragitemno) {
|
||||
var draghome = this.drag_item_home(dragitemno);
|
||||
if (draghome === null) {
|
||||
return null;
|
||||
}
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('dragitemhomes' + dragitemno);
|
||||
drag.addClass('dragitems' + dragitemno);
|
||||
drag.addClass('draginstance' + draginstanceno);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('drag');
|
||||
drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
|
||||
drag.setData('draginstanceno', draginstanceno);
|
||||
drag.setData('dragitemno', dragitemno);
|
||||
draghome.get('parentNode').appendChild(drag);
|
||||
return drag;
|
||||
},
|
||||
draggable_for_question : function (drag, group, choice) {
|
||||
new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'point',
|
||||
groups: [group]
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
|
||||
|
||||
drag.setData('group', group);
|
||||
drag.setData('choice', choice);
|
||||
},
|
||||
draggable_for_form : function (drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'point'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
|
||||
dd.on('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
var draginstanceno = dragnode.getData('draginstanceno');
|
||||
var gooddrop = dragnode.getData('gooddrop');
|
||||
|
||||
if (!gooddrop) {
|
||||
mainobj.reset_drag_xy(draginstanceno);
|
||||
} else {
|
||||
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
|
||||
}
|
||||
}, this);
|
||||
dd.on('drag:start', function(e) {
|
||||
var drag = e.target;
|
||||
drag.get('node').setData('gooddrop', false);
|
||||
}, this);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
update_padding_sizes_all : function () {
|
||||
for (var groupno = 1; groupno <= 8; groupno++) {
|
||||
this.update_padding_size_for_group(groupno);
|
||||
}
|
||||
},
|
||||
update_padding_size_for_group : function (groupno) {
|
||||
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
|
||||
if (groupitems.size() !== 0) {
|
||||
var maxwidth = 0;
|
||||
var maxheight = 0;
|
||||
groupitems.each(function(item){
|
||||
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
|
||||
maxheight = Math.max(maxheight, item.get('clientHeight'));
|
||||
}, this);
|
||||
groupitems.each(function(item) {
|
||||
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
|
||||
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
|
||||
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
|
||||
margintopbottom + 'px ' + marginleftright + 'px');
|
||||
}, this);
|
||||
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
|
||||
'height': maxheight + 10});
|
||||
}
|
||||
},
|
||||
convert_to_window_xy : function (bgimgxy) {
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
}
|
||||
}, {
|
||||
NAME : DDIMAGEORTEXTDDNAME,
|
||||
ATTRS : {
|
||||
drops : {value : null},
|
||||
readonly : {value : false},
|
||||
topnode : {value : null}
|
||||
}
|
||||
});
|
||||
|
||||
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
|
||||
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
|
||||
|
||||
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
|
||||
var DDIMAGEORTEXT_QUESTION = function() {
|
||||
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
|
||||
touchscrolldisable: null,
|
||||
pendingid: '',
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.create_all_drag_and_drops);
|
||||
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.create_all_drag_and_drops);
|
||||
Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
|
||||
},
|
||||
|
||||
/**
|
||||
* prevent_touchmove_from_scrolling allows users of touch screen devices to
|
||||
* use drag and drop and normal scrolling at the same time. I.e. when
|
||||
* touching and dragging a draggable item, the screen does not scroll, but
|
||||
* you can scroll by touching other area of the screen apart from the
|
||||
* draggable items.
|
||||
*/
|
||||
prevent_touchmove_from_scrolling : function(drag) {
|
||||
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
|
||||
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
|
||||
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
|
||||
|
||||
// Disable scrolling when touching the draggable items.
|
||||
drag.on(touchstart, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
return; // Already disabled.
|
||||
}
|
||||
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
});
|
||||
}, this);
|
||||
|
||||
// Allow scrolling after releasing the draggable items.
|
||||
drag.on(touchend, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
this.touchscrolldisable.detach();
|
||||
this.touchscrolldisable = null;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
create_all_drag_and_drops : function () {
|
||||
this.init_drops();
|
||||
this.update_padding_sizes_all();
|
||||
var i = 0;
|
||||
this.doc.drag_item_homes().each(function(dragitemhome){
|
||||
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
|
||||
var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
|
||||
var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
|
||||
var groupsize = this.doc.drop_zone_group(group).size();
|
||||
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
|
||||
i++;
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.draggable_for_question(dragnode, group, choice);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(dragnode);
|
||||
}
|
||||
if (dragnode.hasClass('infinite')) {
|
||||
var dragstocreate = groupsize - 1;
|
||||
while (dragstocreate > 0) {
|
||||
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
|
||||
i++;
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.draggable_for_question(dragnode, group, choice);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(dragnode);
|
||||
}
|
||||
dragstocreate--;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.reposition_drags_for_question();
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.drop_zones().set('tabIndex', 0);
|
||||
this.doc.drop_zones().each(
|
||||
function(v){
|
||||
v.on('dragchange', this.drop_zone_key_press, this);
|
||||
}, this);
|
||||
}
|
||||
M.util.js_complete(this.pendingid);
|
||||
},
|
||||
drop_zone_key_press : function (e) {
|
||||
switch (e.direction) {
|
||||
case 'next' :
|
||||
this.place_next_drag_in(e.target);
|
||||
break;
|
||||
case 'previous' :
|
||||
this.place_previous_drag_in(e.target);
|
||||
break;
|
||||
case 'remove' :
|
||||
this.remove_drag_from_drop(e.target);
|
||||
break;
|
||||
}
|
||||
e.preventDefault();
|
||||
this.reposition_drags_for_question();
|
||||
},
|
||||
place_next_drag_in : function (drop) {
|
||||
this.search_for_unplaced_drop_choice(drop, 1);
|
||||
},
|
||||
place_previous_drag_in : function (drop) {
|
||||
this.search_for_unplaced_drop_choice(drop, -1);
|
||||
},
|
||||
search_for_unplaced_drop_choice : function (drop, direction) {
|
||||
var next;
|
||||
var current = this.current_drag_in_drop(drop);
|
||||
if ('' === current) {
|
||||
if (direction === 1) {
|
||||
next = 1;
|
||||
} else {
|
||||
next = 1;
|
||||
var groupno = drop.getData('group');
|
||||
this.doc.drag_items_in_group(groupno).each(function(drag) {
|
||||
next = Math.max(next, drag.getData('choice'));
|
||||
}, this);
|
||||
}
|
||||
} else {
|
||||
next = + current + direction;
|
||||
}
|
||||
var drag;
|
||||
do {
|
||||
if (this.get_choices_for_drop(next, drop).size() === 0){
|
||||
this.remove_drag_from_drop(drop);
|
||||
return;
|
||||
} else {
|
||||
drag = this.get_unplaced_choice_for_drop(next, drop);
|
||||
}
|
||||
next = next + direction;
|
||||
} while (drag === null);
|
||||
this.place_drag_in_drop(drag, drop);
|
||||
},
|
||||
current_drag_in_drop : function (drop) {
|
||||
var inputid = drop.getData('inputid');
|
||||
var inputnode = Y.one('input#' + inputid);
|
||||
return inputnode.get('value');
|
||||
},
|
||||
remove_drag_from_drop : function (drop) {
|
||||
this.place_drag_in_drop(null, drop);
|
||||
},
|
||||
place_drag_in_drop : function (drag, drop) {
|
||||
var inputid = drop.getData('inputid');
|
||||
var inputnode = Y.one('input#' + inputid);
|
||||
if (drag !== null) {
|
||||
inputnode.set('value', drag.getData('choice'));
|
||||
} else {
|
||||
inputnode.set('value', '');
|
||||
}
|
||||
},
|
||||
reposition_drags_for_question : function() {
|
||||
this.doc.drag_items().removeClass('placed');
|
||||
this.doc.drag_items().each (function (dragitem) {
|
||||
if (dragitem.dd !== undefined) {
|
||||
dragitem.dd.detachAll('drag:start');
|
||||
}
|
||||
}, this);
|
||||
this.doc.drop_zones().each(function(dropzone) {
|
||||
var relativexy = dropzone.getData('xy');
|
||||
dropzone.setXY(this.convert_to_window_xy(relativexy));
|
||||
var inputcss = 'input#' + dropzone.getData('inputid');
|
||||
var input = this.doc.top_node().one(inputcss);
|
||||
var choice = input.get('value');
|
||||
if (choice !== "") {
|
||||
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
|
||||
if (dragitem !== null) {
|
||||
dragitem.setXY(dropzone.getXY());
|
||||
dragitem.addClass('placed');
|
||||
if (dragitem.dd !== undefined) {
|
||||
dragitem.dd.once('drag:start', function (e, input) {
|
||||
input.set('value', '');
|
||||
e.target.get('node').removeClass('placed');
|
||||
},this, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(dragitem) {
|
||||
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
|
||||
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
|
||||
dragitem.setXY(dragitemhome.getXY());
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
get_choices_for_drop : function(choice, drop) {
|
||||
var group = drop.getData('group');
|
||||
return this.doc.top_node().all(
|
||||
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
|
||||
},
|
||||
get_unplaced_choice_for_drop : function(choice, drop) {
|
||||
var dragitems = this.get_choices_for_drop(choice, drop);
|
||||
var dragitem = null;
|
||||
dragitems.some(function (d) {
|
||||
if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
|
||||
dragitem = d;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return dragitem;
|
||||
},
|
||||
init_drops : function () {
|
||||
var dropareas = this.doc.top_node().one('div.dropzones');
|
||||
var groupnodes = {};
|
||||
for (var groupno = 1; groupno <= 8; groupno++) {
|
||||
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
|
||||
dropareas.append(groupnode);
|
||||
groupnodes[groupno] = groupnode;
|
||||
}
|
||||
var drop_hit_handler = function(e) {
|
||||
var drag = e.drag.get('node');
|
||||
var drop = e.drop.get('node');
|
||||
if (Number(drop.getData('group')) === drag.getData('group')){
|
||||
this.place_drag_in_drop(drag, drop);
|
||||
}
|
||||
};
|
||||
for (var dropno in this.get('drops')) {
|
||||
var drop = this.get('drops')[dropno];
|
||||
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
|
||||
var title = drop.text.replace('"', '\"');
|
||||
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '"> </div>';
|
||||
var dropnode = Y.Node.create(dropnodehtml);
|
||||
groupnodes[drop.group].append(dropnode);
|
||||
dropnode.setStyles({'opacity': 0.5});
|
||||
dropnode.setData('xy', drop.xy);
|
||||
dropnode.setData('place', dropno);
|
||||
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
|
||||
dropnode.setData('group', drop.group);
|
||||
var dropdd = new Y.DD.Drop({
|
||||
node: dropnode, groups : [drop.group]});
|
||||
dropdd.on('drop:hit', drop_hit_handler, this);
|
||||
}
|
||||
}
|
||||
}, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
|
||||
|
||||
Y.Event.define('dragchange', {
|
||||
// Webkit and IE repeat keydown when you hold down arrow keys.
|
||||
// Opera links keypress to page scroll; others keydown.
|
||||
// Firefox prevents page scroll via preventDefault() on either
|
||||
// keydown or keypress.
|
||||
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
|
||||
|
||||
_keys: {
|
||||
'32': 'next', // Space
|
||||
'37': 'previous', // Left arrow
|
||||
'38': 'previous', // Up arrow
|
||||
'39': 'next', // Right arrow
|
||||
'40': 'next', // Down arrow
|
||||
'27': 'remove' // Escape
|
||||
},
|
||||
|
||||
_keyHandler: function (e, notifier) {
|
||||
if (this._keys[e.keyCode]) {
|
||||
e.direction = this._keys[e.keyCode];
|
||||
notifier.fire(e);
|
||||
}
|
||||
},
|
||||
|
||||
on: function (node, sub, notifier) {
|
||||
sub._detacher = node.on(this._event, this._keyHandler,
|
||||
this, notifier);
|
||||
}
|
||||
});
|
||||
|
||||
M.qtype_ddimageortext.init_question = function(config) {
|
||||
return new DDIMAGEORTEXT_QUESTION(config);
|
||||
};
|
||||
|
||||
}, '@VERSION@', {"requires": ["node", "dd", "dd-drop", "dd-constrain"]});
|
||||
@@ -0,0 +1,358 @@
|
||||
YUI.add('moodle-qtype_ddimageortext-form', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
|
||||
var DDIMAGEORTEXT_FORM = function() {
|
||||
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
|
||||
pendingid: '',
|
||||
fp : null,
|
||||
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
|
||||
'<div class="dropzones"></div></div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.draw_dd_area();
|
||||
},
|
||||
|
||||
draw_dd_area : function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
this.load_drag_homes();
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
//Listen for a drop:hit on the background image
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
|
||||
this.doc.drag_item_homes()
|
||||
.on('load', this.constrain_image_size, this, 'dragimage');
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
true, 0, this.after_all_images_loaded);
|
||||
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
|
||||
true, 0, this.after_all_images_loaded);
|
||||
} else {
|
||||
this.setup_form_events();
|
||||
M.util.js_complete(this.pendingid);
|
||||
}
|
||||
this.update_visibility_of_file_pickers();
|
||||
},
|
||||
|
||||
after_all_images_loaded : function () {
|
||||
this.update_padding_sizes_all();
|
||||
this.update_drag_instances();
|
||||
this.reposition_drags_for_form();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.reposition_drags_for_form, [], true);
|
||||
},
|
||||
|
||||
constrain_image_size : function (e, imagetype) {
|
||||
var maxsize = this.get('maxsizes')[imagetype];
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
load_drag_homes : function () {
|
||||
// Set up drag items homes.
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
this.load_drag_home(i);
|
||||
}
|
||||
},
|
||||
|
||||
load_drag_home : function (dragitemno) {
|
||||
var url = null;
|
||||
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
|
||||
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
|
||||
}
|
||||
this.doc.add_or_update_drag_item_home(dragitemno, url,
|
||||
this.form.get_form_value('draglabel', [dragitemno]),
|
||||
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
|
||||
},
|
||||
|
||||
update_drag_instances : function () {
|
||||
// Set up drop zones.
|
||||
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
|
||||
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
|
||||
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
|
||||
if (drag !== null) {
|
||||
this.doc.draggable_for_form(drag);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors : function () {
|
||||
var dragitemsoptions = {0: ''};
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.form.get_form_value('draglabel', [i]);
|
||||
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
|
||||
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
|
||||
&& file.name !== null) {
|
||||
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
|
||||
} else if (label !== '') {
|
||||
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
var selector = Y.one('#id_drops_' + i + '_choice');
|
||||
var selectedvalue = selector.get('value');
|
||||
selector.all('option').remove(true);
|
||||
for (var value in dragitemsoptions) {
|
||||
value = + value;
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
if (value === + selectedvalue) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
if (value !== 0) { // No item option is always selectable.
|
||||
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
|
||||
if (cbel && !cbel.get('checked')) {
|
||||
Y.all('fieldset#id_dropzoneheader select').some(function (selector) {
|
||||
if (Number(selector.get('value')) === value) {
|
||||
optionnode.set('disabled', true);
|
||||
return true; // Stop looping.
|
||||
}
|
||||
return false;
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events : function () {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events : function () {
|
||||
// Events triggered by changes to form data.
|
||||
|
||||
// X and y coordinates.
|
||||
Y.all('fieldset#id_dropzoneheader input').on('blur', function (e) {
|
||||
var name = e.target.getAttribute('name');
|
||||
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
|
||||
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
|
||||
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
|
||||
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function (e) {
|
||||
var name = e.target.getAttribute('name');
|
||||
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
|
||||
var old = this.doc.drag_item(draginstanceno);
|
||||
if (old !== null) {
|
||||
old.remove(true);
|
||||
}
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
// Change to group selector.
|
||||
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on(
|
||||
'change', function () {
|
||||
this.doc.drag_items().remove(true);
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on(
|
||||
'change', function () {
|
||||
this.doc.drag_items().remove(true);
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
|
||||
.on('blur', this.set_options_for_drag_item_selectors, this);
|
||||
// Change to infinite checkbox.
|
||||
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
|
||||
.on('change', this.set_options_for_drag_item_selectors, this);
|
||||
}
|
||||
// Event on file picker new file selection.
|
||||
Y.after(function (e) {
|
||||
var name = this.fp.name(e.id);
|
||||
if (name !== 'bgimage') {
|
||||
this.doc.drag_items().remove(true);
|
||||
}
|
||||
this.draw_dd_area();
|
||||
}, M.form_filepicker, 'callback', this);
|
||||
},
|
||||
|
||||
update_visibility_of_file_pickers : function() {
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
|
||||
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
|
||||
.setStyle('display', 'block');
|
||||
} else {
|
||||
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
|
||||
.setStyle('display', 'none');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reposition_drags_for_form : function() {
|
||||
this.doc.drag_items().each(function (drag) {
|
||||
var draginstanceno = drag.getData('draginstanceno');
|
||||
this.reposition_drag_for_form(draginstanceno);
|
||||
}, this);
|
||||
M.util.js_complete(this.pendingid);
|
||||
},
|
||||
|
||||
reposition_drag_for_form : function (draginstanceno) {
|
||||
var drag = this.doc.drag_item(draginstanceno);
|
||||
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
|
||||
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
|
||||
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
|
||||
if (fromform[0] === '' && fromform[1] === '') {
|
||||
var dragitemno = drag.getData('dragitemno');
|
||||
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
|
||||
} else {
|
||||
drag.setXY(this.convert_to_window_xy(fromform));
|
||||
}
|
||||
}
|
||||
},
|
||||
set_drag_xy : function (draginstanceno, xy) {
|
||||
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
|
||||
},
|
||||
reset_drag_xy : function (draginstanceno) {
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
|
||||
},
|
||||
|
||||
//make sure xy value is not out of bounds of bg image
|
||||
constrain_xy : function (draginstanceno, bgimgxy) {
|
||||
var drag = this.doc.drag_item(draginstanceno);
|
||||
var xleftconstrained =
|
||||
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
|
||||
var ytopconstrained =
|
||||
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
|
||||
xleftconstrained = Math.max(xleftconstrained, 0);
|
||||
ytopconstrained = Math.max(ytopconstrained, 0);
|
||||
return [xleftconstrained, ytopconstrained];
|
||||
},
|
||||
convert_to_bg_img_xy : function (windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form : {
|
||||
to_name_with_index : function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el : function (name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value : function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value : function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index : function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers : function () {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file : function (name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href : null, name : null};
|
||||
}
|
||||
},
|
||||
name : function (draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
}, {NAME : DDIMAGEORTEXTFORMNAME, ATTRS : {maxsizes:{value:null}}});
|
||||
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
|
||||
M.qtype_ddimageortext.init_form = function(config) {
|
||||
return new DDIMAGEORTEXT_FORM(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["moodle-qtype_ddimageortext-dd", "form_filepicker"]});
|
||||
@@ -0,0 +1,358 @@
|
||||
YUI.add('moodle-qtype_ddimageortext-form', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
|
||||
var DDIMAGEORTEXT_FORM = function() {
|
||||
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
|
||||
pendingid: '',
|
||||
fp : null,
|
||||
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
|
||||
'<div class="dropzones"></div></div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.draw_dd_area();
|
||||
},
|
||||
|
||||
draw_dd_area : function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
this.load_drag_homes();
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
//Listen for a drop:hit on the background image
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
|
||||
this.doc.drag_item_homes()
|
||||
.on('load', this.constrain_image_size, this, 'dragimage');
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
true, 0, this.after_all_images_loaded);
|
||||
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
|
||||
true, 0, this.after_all_images_loaded);
|
||||
} else {
|
||||
this.setup_form_events();
|
||||
M.util.js_complete(this.pendingid);
|
||||
}
|
||||
this.update_visibility_of_file_pickers();
|
||||
},
|
||||
|
||||
after_all_images_loaded : function () {
|
||||
this.update_padding_sizes_all();
|
||||
this.update_drag_instances();
|
||||
this.reposition_drags_for_form();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.reposition_drags_for_form, [], true);
|
||||
},
|
||||
|
||||
constrain_image_size : function (e, imagetype) {
|
||||
var maxsize = this.get('maxsizes')[imagetype];
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
load_drag_homes : function () {
|
||||
// Set up drag items homes.
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
this.load_drag_home(i);
|
||||
}
|
||||
},
|
||||
|
||||
load_drag_home : function (dragitemno) {
|
||||
var url = null;
|
||||
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
|
||||
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
|
||||
}
|
||||
this.doc.add_or_update_drag_item_home(dragitemno, url,
|
||||
this.form.get_form_value('draglabel', [dragitemno]),
|
||||
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
|
||||
},
|
||||
|
||||
update_drag_instances : function () {
|
||||
// Set up drop zones.
|
||||
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
|
||||
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
|
||||
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
|
||||
if (drag !== null) {
|
||||
this.doc.draggable_for_form(drag);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors : function () {
|
||||
var dragitemsoptions = {0: ''};
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.form.get_form_value('draglabel', [i]);
|
||||
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
|
||||
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
|
||||
&& file.name !== null) {
|
||||
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
|
||||
} else if (label !== '') {
|
||||
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
var selector = Y.one('#id_drops_' + i + '_choice');
|
||||
var selectedvalue = selector.get('value');
|
||||
selector.all('option').remove(true);
|
||||
for (var value in dragitemsoptions) {
|
||||
value = + value;
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
if (value === + selectedvalue) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
if (value !== 0) { // No item option is always selectable.
|
||||
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
|
||||
if (cbel && !cbel.get('checked')) {
|
||||
Y.all('fieldset#id_dropzoneheader select').some(function (selector) {
|
||||
if (Number(selector.get('value')) === value) {
|
||||
optionnode.set('disabled', true);
|
||||
return true; // Stop looping.
|
||||
}
|
||||
return false;
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events : function () {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events : function () {
|
||||
// Events triggered by changes to form data.
|
||||
|
||||
// X and y coordinates.
|
||||
Y.all('fieldset#id_dropzoneheader input').on('blur', function (e) {
|
||||
var name = e.target.getAttribute('name');
|
||||
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
|
||||
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
|
||||
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
|
||||
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function (e) {
|
||||
var name = e.target.getAttribute('name');
|
||||
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
|
||||
var old = this.doc.drag_item(draginstanceno);
|
||||
if (old !== null) {
|
||||
old.remove(true);
|
||||
}
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
// Change to group selector.
|
||||
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on(
|
||||
'change', function () {
|
||||
this.doc.drag_items().remove(true);
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on(
|
||||
'change', function () {
|
||||
this.doc.drag_items().remove(true);
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
|
||||
.on('blur', this.set_options_for_drag_item_selectors, this);
|
||||
// Change to infinite checkbox.
|
||||
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
|
||||
.on('change', this.set_options_for_drag_item_selectors, this);
|
||||
}
|
||||
// Event on file picker new file selection.
|
||||
Y.after(function (e) {
|
||||
var name = this.fp.name(e.id);
|
||||
if (name !== 'bgimage') {
|
||||
this.doc.drag_items().remove(true);
|
||||
}
|
||||
this.draw_dd_area();
|
||||
}, M.form_filepicker, 'callback', this);
|
||||
},
|
||||
|
||||
update_visibility_of_file_pickers : function() {
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
|
||||
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
|
||||
.setStyle('display', 'block');
|
||||
} else {
|
||||
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
|
||||
.setStyle('display', 'none');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reposition_drags_for_form : function() {
|
||||
this.doc.drag_items().each(function (drag) {
|
||||
var draginstanceno = drag.getData('draginstanceno');
|
||||
this.reposition_drag_for_form(draginstanceno);
|
||||
}, this);
|
||||
M.util.js_complete(this.pendingid);
|
||||
},
|
||||
|
||||
reposition_drag_for_form : function (draginstanceno) {
|
||||
var drag = this.doc.drag_item(draginstanceno);
|
||||
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
|
||||
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
|
||||
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
|
||||
if (fromform[0] === '' && fromform[1] === '') {
|
||||
var dragitemno = drag.getData('dragitemno');
|
||||
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
|
||||
} else {
|
||||
drag.setXY(this.convert_to_window_xy(fromform));
|
||||
}
|
||||
}
|
||||
},
|
||||
set_drag_xy : function (draginstanceno, xy) {
|
||||
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
|
||||
},
|
||||
reset_drag_xy : function (draginstanceno) {
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
|
||||
},
|
||||
|
||||
//make sure xy value is not out of bounds of bg image
|
||||
constrain_xy : function (draginstanceno, bgimgxy) {
|
||||
var drag = this.doc.drag_item(draginstanceno);
|
||||
var xleftconstrained =
|
||||
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
|
||||
var ytopconstrained =
|
||||
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
|
||||
xleftconstrained = Math.max(xleftconstrained, 0);
|
||||
ytopconstrained = Math.max(ytopconstrained, 0);
|
||||
return [xleftconstrained, ytopconstrained];
|
||||
},
|
||||
convert_to_bg_img_xy : function (windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form : {
|
||||
to_name_with_index : function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el : function (name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value : function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value : function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index : function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers : function () {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file : function (name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href : null, name : null};
|
||||
}
|
||||
},
|
||||
name : function (draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
}, {NAME : DDIMAGEORTEXTFORMNAME, ATTRS : {maxsizes:{value:null}}});
|
||||
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
|
||||
M.qtype_ddimageortext.init_form = function(config) {
|
||||
return new DDIMAGEORTEXT_FORM(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["moodle-qtype_ddimageortext-dd", "form_filepicker"]});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "moodle-qtype_ddimageortext-dd",
|
||||
"builds": {
|
||||
"moodle-qtype_ddimageortext-dd": {
|
||||
"jsfiles": [
|
||||
"ddimageortext.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
// 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/>.
|
||||
|
||||
var DDIMAGEORTEXTDDNAME = 'ddimageortext_dd';
|
||||
var DDIMAGEORTEXT_DD = function() {
|
||||
DDIMAGEORTEXT_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDIMAGEORTEXT_DD, Y.Base, {
|
||||
doc : null,
|
||||
polltimer : null,
|
||||
afterimageloaddone : false,
|
||||
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
var alldragsloaded = !this.doc.drag_item_homes().some(function(dragitemhome){
|
||||
//in 'some' loop returning true breaks the loop and is passed as return value from
|
||||
//'some' else returns false. Can be though of as equivalent to ||.
|
||||
if (dragitemhome.get('tagName') !== 'IMG'){
|
||||
return false;
|
||||
}
|
||||
var done = (dragitemhome.get('complete'));
|
||||
if (waitforimageconstrain) {
|
||||
done = done && dragitemhome.hasClass('constrained');
|
||||
}
|
||||
return !done;
|
||||
});
|
||||
if (bgdone && alldragsloaded) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.drag_item_homes().detach('load', this.poll_for_image_load);
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure : function (mainobj) {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node : function() {
|
||||
return topnode;
|
||||
},
|
||||
drag_items : function() {
|
||||
return dragitemsarea.all('.drag');
|
||||
},
|
||||
drop_zones : function() {
|
||||
return topnode.all('div.dropzones div.dropzone');
|
||||
},
|
||||
drop_zone_group : function(groupno) {
|
||||
return topnode.all('div.dropzones div.group' + groupno);
|
||||
},
|
||||
drag_items_cloned_from : function(dragitemno) {
|
||||
return dragitemsarea.all('.dragitems' + dragitemno);
|
||||
},
|
||||
drag_item : function(draginstanceno) {
|
||||
return dragitemsarea.one('.draginstance' + draginstanceno);
|
||||
},
|
||||
drag_items_in_group : function(groupno) {
|
||||
return dragitemsarea.all('.drag.group' + groupno);
|
||||
},
|
||||
drag_item_homes : function() {
|
||||
return dragitemsarea.all('.draghome');
|
||||
},
|
||||
bg_img : function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img : function (url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
add_or_update_drag_item_home : function (dragitemno, url, alt, group) {
|
||||
var oldhome = this.drag_item_home(dragitemno);
|
||||
var classes = 'draghome dragitemhomes' + dragitemno + ' group' + group;
|
||||
var imghtml = '<img class="' + classes + '" src="' + url + '" alt="' + alt + '" />';
|
||||
var divhtml = '<div class="' + classes + '">' + alt + '</div>';
|
||||
if (oldhome === null) {
|
||||
if (url) {
|
||||
dragitemsarea.append(imghtml);
|
||||
} else if (alt !== '') {
|
||||
dragitemsarea.append(divhtml);
|
||||
}
|
||||
} else {
|
||||
if (url) {
|
||||
dragitemsarea.insert(imghtml, oldhome);
|
||||
} else if (alt !== '') {
|
||||
dragitemsarea.insert(divhtml, oldhome);
|
||||
}
|
||||
oldhome.remove(true);
|
||||
}
|
||||
var newlycreated = dragitemsarea.one('.dragitemhomes' + dragitemno);
|
||||
if (newlycreated !== null) {
|
||||
newlycreated.setData('groupno', group);
|
||||
newlycreated.setData('dragitemno', dragitemno);
|
||||
}
|
||||
},
|
||||
drag_item_home : function (dragitemno) {
|
||||
return dragitemsarea.one('.dragitemhomes' + dragitemno);
|
||||
},
|
||||
get_classname_numeric_suffix : function(node, prefix) {
|
||||
var classes = node.getAttribute('class');
|
||||
if (classes !== '') {
|
||||
var classesarr = classes.split(' ');
|
||||
for (var index = 0; index < classesarr.length; index++) {
|
||||
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
|
||||
if (patt1.test(classesarr[index])) {
|
||||
var patt2 = new RegExp('([0-9])+$');
|
||||
var match = patt2.exec(classesarr[index]);
|
||||
return + match[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'Prefix "' + prefix + '" not found in class names.';
|
||||
},
|
||||
clone_new_drag_item : function (draginstanceno, dragitemno) {
|
||||
var draghome = this.drag_item_home(dragitemno);
|
||||
if (draghome === null) {
|
||||
return null;
|
||||
}
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('dragitemhomes' + dragitemno);
|
||||
drag.addClass('dragitems' + dragitemno);
|
||||
drag.addClass('draginstance' + draginstanceno);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('drag');
|
||||
drag.setStyles({'visibility': 'visible', 'position' : 'absolute'});
|
||||
drag.setData('draginstanceno', draginstanceno);
|
||||
drag.setData('dragitemno', dragitemno);
|
||||
draghome.get('parentNode').appendChild(drag);
|
||||
return drag;
|
||||
},
|
||||
draggable_for_question : function (drag, group, choice) {
|
||||
new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'point',
|
||||
groups: [group]
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
|
||||
|
||||
drag.setData('group', group);
|
||||
drag.setData('choice', choice);
|
||||
},
|
||||
draggable_for_form : function (drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'point'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: topnode});
|
||||
dd.on('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
var draginstanceno = dragnode.getData('draginstanceno');
|
||||
var gooddrop = dragnode.getData('gooddrop');
|
||||
|
||||
if (!gooddrop) {
|
||||
mainobj.reset_drag_xy(draginstanceno);
|
||||
} else {
|
||||
mainobj.set_drag_xy(draginstanceno, [e.pageX, e.pageY]);
|
||||
}
|
||||
}, this);
|
||||
dd.on('drag:start', function(e) {
|
||||
var drag = e.target;
|
||||
drag.get('node').setData('gooddrop', false);
|
||||
}, this);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
update_padding_sizes_all : function () {
|
||||
for (var groupno = 1; groupno <= 8; groupno++) {
|
||||
this.update_padding_size_for_group(groupno);
|
||||
}
|
||||
},
|
||||
update_padding_size_for_group : function (groupno) {
|
||||
var groupitems = this.doc.top_node().all('.draghome.group' + groupno);
|
||||
if (groupitems.size() !== 0) {
|
||||
var maxwidth = 0;
|
||||
var maxheight = 0;
|
||||
groupitems.each(function(item){
|
||||
maxwidth = Math.max(maxwidth, item.get('clientWidth'));
|
||||
maxheight = Math.max(maxheight, item.get('clientHeight'));
|
||||
}, this);
|
||||
groupitems.each(function(item) {
|
||||
var margintopbottom = Math.round((10 + maxheight - item.get('clientHeight')) / 2);
|
||||
var marginleftright = Math.round((10 + maxwidth - item.get('clientWidth')) / 2);
|
||||
item.setStyle('padding', margintopbottom + 'px ' + marginleftright + 'px ' +
|
||||
margintopbottom + 'px ' + marginleftright + 'px');
|
||||
}, this);
|
||||
this.doc.drop_zone_group(groupno).setStyles({'width': maxwidth + 10,
|
||||
'height': maxheight + 10});
|
||||
}
|
||||
},
|
||||
convert_to_window_xy : function (bgimgxy) {
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
}
|
||||
}, {
|
||||
NAME : DDIMAGEORTEXTDDNAME,
|
||||
ATTRS : {
|
||||
drops : {value : null},
|
||||
readonly : {value : false},
|
||||
topnode : {value : null}
|
||||
}
|
||||
});
|
||||
|
||||
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
|
||||
M.qtype_ddimageortext.dd_base_class = DDIMAGEORTEXT_DD;
|
||||
|
||||
var DDIMAGEORTEXTQUESTIONNAME = 'ddimageortext_question';
|
||||
var DDIMAGEORTEXT_QUESTION = function() {
|
||||
DDIMAGEORTEXT_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDIMAGEORTEXT_QUESTION, M.qtype_ddimageortext.dd_base_class, {
|
||||
touchscrolldisable: null,
|
||||
pendingid: '',
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddimageortext-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.create_all_drag_and_drops);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.create_all_drag_and_drops);
|
||||
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.create_all_drag_and_drops);
|
||||
Y.later(500, this, this.reposition_drags_for_question, [this.pendingid], true);
|
||||
},
|
||||
|
||||
/**
|
||||
* prevent_touchmove_from_scrolling allows users of touch screen devices to
|
||||
* use drag and drop and normal scrolling at the same time. I.e. when
|
||||
* touching and dragging a draggable item, the screen does not scroll, but
|
||||
* you can scroll by touching other area of the screen apart from the
|
||||
* draggable items.
|
||||
*/
|
||||
prevent_touchmove_from_scrolling : function(drag) {
|
||||
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
|
||||
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
|
||||
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
|
||||
|
||||
// Disable scrolling when touching the draggable items.
|
||||
drag.on(touchstart, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
return; // Already disabled.
|
||||
}
|
||||
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
});
|
||||
}, this);
|
||||
|
||||
// Allow scrolling after releasing the draggable items.
|
||||
drag.on(touchend, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
this.touchscrolldisable.detach();
|
||||
this.touchscrolldisable = null;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
create_all_drag_and_drops : function () {
|
||||
this.init_drops();
|
||||
this.update_padding_sizes_all();
|
||||
var i = 0;
|
||||
this.doc.drag_item_homes().each(function(dragitemhome){
|
||||
var dragitemno = Number(this.doc.get_classname_numeric_suffix(dragitemhome, 'dragitemhomes'));
|
||||
var choice = + this.doc.get_classname_numeric_suffix(dragitemhome, 'choice');
|
||||
var group = + this.doc.get_classname_numeric_suffix(dragitemhome, 'group');
|
||||
var groupsize = this.doc.drop_zone_group(group).size();
|
||||
var dragnode = this.doc.clone_new_drag_item(i, dragitemno);
|
||||
i++;
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.draggable_for_question(dragnode, group, choice);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(dragnode);
|
||||
}
|
||||
if (dragnode.hasClass('infinite')) {
|
||||
var dragstocreate = groupsize - 1;
|
||||
while (dragstocreate > 0) {
|
||||
dragnode = this.doc.clone_new_drag_item(i, dragitemno);
|
||||
i++;
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.draggable_for_question(dragnode, group, choice);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(dragnode);
|
||||
}
|
||||
dragstocreate--;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.reposition_drags_for_question();
|
||||
if (!this.get('readonly')) {
|
||||
this.doc.drop_zones().set('tabIndex', 0);
|
||||
this.doc.drop_zones().each(
|
||||
function(v){
|
||||
v.on('dragchange', this.drop_zone_key_press, this);
|
||||
}, this);
|
||||
}
|
||||
M.util.js_complete(this.pendingid);
|
||||
},
|
||||
drop_zone_key_press : function (e) {
|
||||
switch (e.direction) {
|
||||
case 'next' :
|
||||
this.place_next_drag_in(e.target);
|
||||
break;
|
||||
case 'previous' :
|
||||
this.place_previous_drag_in(e.target);
|
||||
break;
|
||||
case 'remove' :
|
||||
this.remove_drag_from_drop(e.target);
|
||||
break;
|
||||
}
|
||||
e.preventDefault();
|
||||
this.reposition_drags_for_question();
|
||||
},
|
||||
place_next_drag_in : function (drop) {
|
||||
this.search_for_unplaced_drop_choice(drop, 1);
|
||||
},
|
||||
place_previous_drag_in : function (drop) {
|
||||
this.search_for_unplaced_drop_choice(drop, -1);
|
||||
},
|
||||
search_for_unplaced_drop_choice : function (drop, direction) {
|
||||
var next;
|
||||
var current = this.current_drag_in_drop(drop);
|
||||
if ('' === current) {
|
||||
if (direction === 1) {
|
||||
next = 1;
|
||||
} else {
|
||||
next = 1;
|
||||
var groupno = drop.getData('group');
|
||||
this.doc.drag_items_in_group(groupno).each(function(drag) {
|
||||
next = Math.max(next, drag.getData('choice'));
|
||||
}, this);
|
||||
}
|
||||
} else {
|
||||
next = + current + direction;
|
||||
}
|
||||
var drag;
|
||||
do {
|
||||
if (this.get_choices_for_drop(next, drop).size() === 0){
|
||||
this.remove_drag_from_drop(drop);
|
||||
return;
|
||||
} else {
|
||||
drag = this.get_unplaced_choice_for_drop(next, drop);
|
||||
}
|
||||
next = next + direction;
|
||||
} while (drag === null);
|
||||
this.place_drag_in_drop(drag, drop);
|
||||
},
|
||||
current_drag_in_drop : function (drop) {
|
||||
var inputid = drop.getData('inputid');
|
||||
var inputnode = Y.one('input#' + inputid);
|
||||
return inputnode.get('value');
|
||||
},
|
||||
remove_drag_from_drop : function (drop) {
|
||||
this.place_drag_in_drop(null, drop);
|
||||
},
|
||||
place_drag_in_drop : function (drag, drop) {
|
||||
var inputid = drop.getData('inputid');
|
||||
var inputnode = Y.one('input#' + inputid);
|
||||
if (drag !== null) {
|
||||
inputnode.set('value', drag.getData('choice'));
|
||||
} else {
|
||||
inputnode.set('value', '');
|
||||
}
|
||||
},
|
||||
reposition_drags_for_question : function() {
|
||||
this.doc.drag_items().removeClass('placed');
|
||||
this.doc.drag_items().each (function (dragitem) {
|
||||
if (dragitem.dd !== undefined) {
|
||||
dragitem.dd.detachAll('drag:start');
|
||||
}
|
||||
}, this);
|
||||
this.doc.drop_zones().each(function(dropzone) {
|
||||
var relativexy = dropzone.getData('xy');
|
||||
dropzone.setXY(this.convert_to_window_xy(relativexy));
|
||||
var inputcss = 'input#' + dropzone.getData('inputid');
|
||||
var input = this.doc.top_node().one(inputcss);
|
||||
var choice = input.get('value');
|
||||
if (choice !== "") {
|
||||
var dragitem = this.get_unplaced_choice_for_drop(choice, dropzone);
|
||||
if (dragitem !== null) {
|
||||
dragitem.setXY(dropzone.getXY());
|
||||
dragitem.addClass('placed');
|
||||
if (dragitem.dd !== undefined) {
|
||||
dragitem.dd.once('drag:start', function (e, input) {
|
||||
input.set('value', '');
|
||||
e.target.get('node').removeClass('placed');
|
||||
},this, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(dragitem) {
|
||||
if (!dragitem.hasClass('placed') && !dragitem.hasClass('yui3-dd-dragging')) {
|
||||
var dragitemhome = this.doc.drag_item_home(dragitem.getData('dragitemno'));
|
||||
dragitem.setXY(dragitemhome.getXY());
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
get_choices_for_drop : function(choice, drop) {
|
||||
var group = drop.getData('group');
|
||||
return this.doc.top_node().all(
|
||||
'div.dragitemgroup' + group + ' .choice' + choice + '.drag');
|
||||
},
|
||||
get_unplaced_choice_for_drop : function(choice, drop) {
|
||||
var dragitems = this.get_choices_for_drop(choice, drop);
|
||||
var dragitem = null;
|
||||
dragitems.some(function (d) {
|
||||
if (!d.hasClass('placed') && !d.hasClass('yui3-dd-dragging')) {
|
||||
dragitem = d;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return dragitem;
|
||||
},
|
||||
init_drops : function () {
|
||||
var dropareas = this.doc.top_node().one('div.dropzones');
|
||||
var groupnodes = {};
|
||||
for (var groupno = 1; groupno <= 8; groupno++) {
|
||||
var groupnode = Y.Node.create('<div class = "dropzonegroup' + groupno + '"></div>');
|
||||
dropareas.append(groupnode);
|
||||
groupnodes[groupno] = groupnode;
|
||||
}
|
||||
var drop_hit_handler = function(e) {
|
||||
var drag = e.drag.get('node');
|
||||
var drop = e.drop.get('node');
|
||||
if (Number(drop.getData('group')) === drag.getData('group')){
|
||||
this.place_drag_in_drop(drag, drop);
|
||||
}
|
||||
};
|
||||
for (var dropno in this.get('drops')) {
|
||||
var drop = this.get('drops')[dropno];
|
||||
var nodeclass = 'dropzone group' + drop.group + ' place' + dropno;
|
||||
var title = drop.text.replace('"', '\"');
|
||||
var dropnodehtml = '<div title="' + title + '" class="' + nodeclass + '"> </div>';
|
||||
var dropnode = Y.Node.create(dropnodehtml);
|
||||
groupnodes[drop.group].append(dropnode);
|
||||
dropnode.setStyles({'opacity': 0.5});
|
||||
dropnode.setData('xy', drop.xy);
|
||||
dropnode.setData('place', dropno);
|
||||
dropnode.setData('inputid', drop.fieldname.replace(':', '_'));
|
||||
dropnode.setData('group', drop.group);
|
||||
var dropdd = new Y.DD.Drop({
|
||||
node: dropnode, groups : [drop.group]});
|
||||
dropdd.on('drop:hit', drop_hit_handler, this);
|
||||
}
|
||||
}
|
||||
}, {NAME : DDIMAGEORTEXTQUESTIONNAME, ATTRS : {}});
|
||||
|
||||
Y.Event.define('dragchange', {
|
||||
// Webkit and IE repeat keydown when you hold down arrow keys.
|
||||
// Opera links keypress to page scroll; others keydown.
|
||||
// Firefox prevents page scroll via preventDefault() on either
|
||||
// keydown or keypress.
|
||||
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
|
||||
|
||||
_keys: {
|
||||
'32': 'next', // Space
|
||||
'37': 'previous', // Left arrow
|
||||
'38': 'previous', // Up arrow
|
||||
'39': 'next', // Right arrow
|
||||
'40': 'next', // Down arrow
|
||||
'27': 'remove' // Escape
|
||||
},
|
||||
|
||||
_keyHandler: function (e, notifier) {
|
||||
if (this._keys[e.keyCode]) {
|
||||
e.direction = this._keys[e.keyCode];
|
||||
notifier.fire(e);
|
||||
}
|
||||
},
|
||||
|
||||
on: function (node, sub, notifier) {
|
||||
sub._detacher = node.on(this._event, this._keyHandler,
|
||||
this, notifier);
|
||||
}
|
||||
});
|
||||
|
||||
M.qtype_ddimageortext.init_question = function(config) {
|
||||
return new DDIMAGEORTEXT_QUESTION(config);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"moodle-qtype_ddimageortext-dd": {
|
||||
"requires": [
|
||||
"node",
|
||||
"dd",
|
||||
"dd-drop",
|
||||
"dd-constrain"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "moodle-qtype_ddimageortext-form",
|
||||
"builds": {
|
||||
"moodle-qtype_ddimageortext-form": {
|
||||
"jsfiles": [
|
||||
"form.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDIMAGEORTEXTFORMNAME = 'moodle-qtype_ddimageortext-form';
|
||||
var DDIMAGEORTEXT_FORM = function() {
|
||||
DDIMAGEORTEXT_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(DDIMAGEORTEXT_FORM, M.qtype_ddimageortext.dd_base_class, {
|
||||
pendingid: '',
|
||||
fp : null,
|
||||
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddimageortext-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append('<div class="ddarea"><div class="droparea"></div><div class="dragitems"></div>' +
|
||||
'<div class="dropzones"></div></div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.draw_dd_area();
|
||||
},
|
||||
|
||||
draw_dd_area : function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
this.load_drag_homes();
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
//Listen for a drop:hit on the background image
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this, 'bgimage');
|
||||
this.doc.drag_item_homes()
|
||||
.on('load', this.constrain_image_size, this, 'dragimage');
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
true, 0, this.after_all_images_loaded);
|
||||
this.doc.drag_item_homes().after('load', this.poll_for_image_load, this,
|
||||
true, 0, this.after_all_images_loaded);
|
||||
} else {
|
||||
this.setup_form_events();
|
||||
M.util.js_complete(this.pendingid);
|
||||
}
|
||||
this.update_visibility_of_file_pickers();
|
||||
},
|
||||
|
||||
after_all_images_loaded : function () {
|
||||
this.update_padding_sizes_all();
|
||||
this.update_drag_instances();
|
||||
this.reposition_drags_for_form();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.reposition_drags_for_form, [], true);
|
||||
},
|
||||
|
||||
constrain_image_size : function (e, imagetype) {
|
||||
var maxsize = this.get('maxsizes')[imagetype];
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
load_drag_homes : function () {
|
||||
// Set up drag items homes.
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
this.load_drag_home(i);
|
||||
}
|
||||
},
|
||||
|
||||
load_drag_home : function (dragitemno) {
|
||||
var url = null;
|
||||
if ('image' === this.form.get_form_value('drags', [dragitemno, 'dragitemtype'])) {
|
||||
url = this.fp.file(this.form.to_name_with_index('dragitem', [dragitemno])).href;
|
||||
}
|
||||
this.doc.add_or_update_drag_item_home(dragitemno, url,
|
||||
this.form.get_form_value('draglabel', [dragitemno]),
|
||||
this.form.get_form_value('drags', [dragitemno, 'draggroup']));
|
||||
},
|
||||
|
||||
update_drag_instances : function () {
|
||||
// Set up drop zones.
|
||||
for (var i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [i, 'choice']);
|
||||
if (dragitemno !== '0' && (this.doc.drag_item(i) === null)) {
|
||||
var drag = this.doc.clone_new_drag_item(i, dragitemno - 1);
|
||||
if (drag !== null) {
|
||||
this.doc.draggable_for_form(drag);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors : function () {
|
||||
var dragitemsoptions = {0: ''};
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.form.get_form_value('draglabel', [i]);
|
||||
var file = this.fp.file(this.form.to_name_with_index('dragitem', [i]));
|
||||
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])
|
||||
&& file.name !== null) {
|
||||
dragitemsoptions[i + 1] = (i + 1) + '. ' + label + ' (' + file.name + ')';
|
||||
} else if (label !== '') {
|
||||
dragitemsoptions[i + 1] = (i + 1) + '. ' + label;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
var selector = Y.one('#id_drops_' + i + '_choice');
|
||||
var selectedvalue = selector.get('value');
|
||||
selector.all('option').remove(true);
|
||||
for (var value in dragitemsoptions) {
|
||||
value = + value;
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
if (value === + selectedvalue) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
if (value !== 0) { // No item option is always selectable.
|
||||
var cbel = Y.one('#id_drags_' + (value - 1) + '_infinite');
|
||||
if (cbel && !cbel.get('checked')) {
|
||||
Y.all('fieldset#id_dropzoneheader select').some(function (selector) {
|
||||
if (Number(selector.get('value')) === value) {
|
||||
optionnode.set('disabled', true);
|
||||
return true; // Stop looping.
|
||||
}
|
||||
return false;
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events : function () {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events : function () {
|
||||
// Events triggered by changes to form data.
|
||||
|
||||
// X and y coordinates.
|
||||
Y.all('fieldset#id_dropzoneheader input').on('blur', function (e) {
|
||||
var name = e.target.getAttribute('name');
|
||||
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
|
||||
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
|
||||
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
|
||||
var constrainedxy = this.constrain_xy(draginstanceno, fromform);
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], constrainedxy[0]);
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], constrainedxy[1]);
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function (e) {
|
||||
var name = e.target.getAttribute('name');
|
||||
var draginstanceno = this.form.from_name_with_index(name).indexes[0];
|
||||
var old = this.doc.drag_item(draginstanceno);
|
||||
if (old !== null) {
|
||||
old.remove(true);
|
||||
}
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
// Change to group selector.
|
||||
Y.all('#fgroup_id_drags_' + i + ' select.draggroup').on(
|
||||
'change', function () {
|
||||
this.doc.drag_items().remove(true);
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
Y.all('#fgroup_id_drags_' + i + ' select.dragitemtype').on(
|
||||
'change', function () {
|
||||
this.doc.drag_items().remove(true);
|
||||
this.draw_dd_area();
|
||||
}, this);
|
||||
Y.all('fieldset#draggableitemheader_' + i + ' input[type="text"]')
|
||||
.on('blur', this.set_options_for_drag_item_selectors, this);
|
||||
// Change to infinite checkbox.
|
||||
Y.all('fieldset#draggableitemheader_' + i + ' input[type="checkbox"]')
|
||||
.on('change', this.set_options_for_drag_item_selectors, this);
|
||||
}
|
||||
// Event on file picker new file selection.
|
||||
Y.after(function (e) {
|
||||
var name = this.fp.name(e.id);
|
||||
if (name !== 'bgimage') {
|
||||
this.doc.drag_items().remove(true);
|
||||
}
|
||||
this.draw_dd_area();
|
||||
}, M.form_filepicker, 'callback', this);
|
||||
},
|
||||
|
||||
update_visibility_of_file_pickers : function() {
|
||||
for (var i = 0; i < this.form.get_form_value('noitems', []); i++) {
|
||||
if ('image' === this.form.get_form_value('drags', [i, 'dragitemtype'])) {
|
||||
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
|
||||
.setStyle('display', 'block');
|
||||
} else {
|
||||
Y.one('input#id_dragitem_' + i).get('parentNode').get('parentNode')
|
||||
.setStyle('display', 'none');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reposition_drags_for_form : function() {
|
||||
this.doc.drag_items().each(function (drag) {
|
||||
var draginstanceno = drag.getData('draginstanceno');
|
||||
this.reposition_drag_for_form(draginstanceno);
|
||||
}, this);
|
||||
M.util.js_complete(this.pendingid);
|
||||
},
|
||||
|
||||
reposition_drag_for_form : function (draginstanceno) {
|
||||
var drag = this.doc.drag_item(draginstanceno);
|
||||
if (null !== drag && !drag.hasClass('yui3-dd-dragging')) {
|
||||
var fromform = [this.form.get_form_value('drops', [draginstanceno, 'xleft']),
|
||||
this.form.get_form_value('drops', [draginstanceno, 'ytop'])];
|
||||
if (fromform[0] === '' && fromform[1] === '') {
|
||||
var dragitemno = drag.getData('dragitemno');
|
||||
drag.setXY(this.doc.drag_item_home(dragitemno).getXY());
|
||||
} else {
|
||||
drag.setXY(this.convert_to_window_xy(fromform));
|
||||
}
|
||||
}
|
||||
},
|
||||
set_drag_xy : function (draginstanceno, xy) {
|
||||
xy = this.constrain_xy(draginstanceno, this.convert_to_bg_img_xy(xy));
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], Math.round(xy[0]));
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], Math.round(xy[1]));
|
||||
},
|
||||
reset_drag_xy : function (draginstanceno) {
|
||||
this.form.set_form_value('drops', [draginstanceno, 'xleft'], '');
|
||||
this.form.set_form_value('drops', [draginstanceno, 'ytop'], '');
|
||||
},
|
||||
|
||||
//make sure xy value is not out of bounds of bg image
|
||||
constrain_xy : function (draginstanceno, bgimgxy) {
|
||||
var drag = this.doc.drag_item(draginstanceno);
|
||||
var xleftconstrained =
|
||||
Math.min(bgimgxy[0], this.doc.bg_img().get('width') - drag.get('offsetWidth'));
|
||||
var ytopconstrained =
|
||||
Math.min(bgimgxy[1], this.doc.bg_img().get('height') - drag.get('offsetHeight'));
|
||||
xleftconstrained = Math.max(xleftconstrained, 0);
|
||||
ytopconstrained = Math.max(ytopconstrained, 0);
|
||||
return [xleftconstrained, ytopconstrained];
|
||||
},
|
||||
convert_to_bg_img_xy : function (windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form : {
|
||||
to_name_with_index : function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el : function (name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value : function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value : function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index : function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers : function () {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file : function (name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href : null, name : null};
|
||||
}
|
||||
},
|
||||
name : function (draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
}, {NAME : DDIMAGEORTEXTFORMNAME, ATTRS : {maxsizes:{value:null}}});
|
||||
M.qtype_ddimageortext = M.qtype_ddimageortext || {};
|
||||
M.qtype_ddimageortext.init_form = function(config) {
|
||||
return new DDIMAGEORTEXT_FORM(config);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"moodle-qtype_ddimageortext-form": {
|
||||
"requires": [
|
||||
"moodle-qtype_ddimageortext-dd",
|
||||
"form_filepicker"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Backup code for qtype_ddmarker.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Provides the information to backup ddmarker questions.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backup_qtype_ddmarker_plugin extends backup_qtype_plugin {
|
||||
/**
|
||||
* Get the name of this question type.
|
||||
*
|
||||
* @return string the question type, like 'ddmarker'.
|
||||
*/
|
||||
protected static function qtype_name() {
|
||||
return 'ddmarker';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the qtype information to attach to question element
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
$qtype = self::qtype_name();
|
||||
$plugin = $this->get_plugin_element(null, '../../qtype', $qtype);
|
||||
|
||||
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
|
||||
|
||||
$plugin->add_child($pluginwrapper);
|
||||
|
||||
$dds = new backup_nested_element($qtype, array('id'), array(
|
||||
'shuffleanswers', 'correctfeedback', 'correctfeedbackformat',
|
||||
'partiallycorrectfeedback', 'partiallycorrectfeedbackformat',
|
||||
'incorrectfeedback', 'incorrectfeedbackformat', 'shownumcorrect',
|
||||
'showmisplaced')
|
||||
);
|
||||
|
||||
$pluginwrapper->add_child($dds);
|
||||
$drags = new backup_nested_element('drags');
|
||||
|
||||
$drag = new backup_nested_element('drag', array('id'),
|
||||
array('no', 'infinite', 'label', 'noofdrags'));
|
||||
$drops = new backup_nested_element('drops');
|
||||
|
||||
$drop = new backup_nested_element('drop', array('id'),
|
||||
array('no', 'shape', 'coords', 'choice'));
|
||||
|
||||
$dds->set_source_table("qtype_{$qtype}",
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
$pluginwrapper->add_child($drags);
|
||||
$drags->add_child($drag);
|
||||
$pluginwrapper->add_child($drops);
|
||||
$drops->add_child($drop);
|
||||
|
||||
$drag->set_source_table("qtype_{$qtype}_drags",
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
$drop->set_source_table("qtype_{$qtype}_drops",
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one array with filearea => mappingname elements for the qtype
|
||||
*
|
||||
* Used by {@link get_components_and_fileareas} to know about all the qtype
|
||||
* files to be processed both in backup and restore.
|
||||
*/
|
||||
public static function get_qtype_fileareas() {
|
||||
$qtype = self::qtype_name();
|
||||
return array(
|
||||
'correctfeedback' => 'question_created',
|
||||
'partiallycorrectfeedback' => 'question_created',
|
||||
'incorrectfeedback' => 'question_created',
|
||||
|
||||
'bgimage' => 'question_created');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Restore code for qtype_ddmarker.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Restore plugin class for the ddmarker question type plugin.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_qtype_ddmarker_plugin extends restore_qtype_plugin {
|
||||
/**
|
||||
* Returns the qtype name.
|
||||
*
|
||||
* @return string The type name
|
||||
*/
|
||||
protected static function qtype_name() {
|
||||
return 'ddmarker';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths to be handled by the plugin at question level.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
|
||||
$paths = array();
|
||||
|
||||
// Add own qtype stuff.
|
||||
$elename = 'dds';
|
||||
$elepath = $this->get_pathfor('/'.self::qtype_name());
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
$elename = 'drag';
|
||||
$elepath = $this->get_pathfor('/drags/drag');
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
$elename = 'drop';
|
||||
$elepath = $this->get_pathfor('/drops/drop');
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
return $paths; // And we return the interesting paths.
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/{qtypename} element.
|
||||
*
|
||||
* @param array|object $data Drag and drop data to work with.
|
||||
*/
|
||||
public function process_dds($data) {
|
||||
global $DB;
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
// If the question has been created by restore
|
||||
// we need to create its qtype_ddmarker too.
|
||||
if ($questioncreated) {
|
||||
// Adjust some columns.
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record($prefix, $data);
|
||||
// Create mapping (needed for decoding links).
|
||||
$this->set_mapping($prefix, $oldid, $newitemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/drags/drag element.
|
||||
*
|
||||
* @param array|object $data Drag and drop drag data to work with.
|
||||
*/
|
||||
public function process_drag($data) {
|
||||
global $DB;
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
if ($questioncreated) {
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record("{$prefix}_drags", $data);
|
||||
// Create mapping (there are files and states based on this).
|
||||
$this->set_mapping("{$prefix}_drags", $oldid, $newitemid);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/drags/drop element.
|
||||
*
|
||||
* @param array|object $data Drag and drop drops data to work with.
|
||||
*/
|
||||
public function process_drop($data) {
|
||||
global $DB;
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
if ($questioncreated) {
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record("{$prefix}_drops", $data);
|
||||
// Create mapping (there are files and states based on this).
|
||||
$this->set_mapping("{$prefix}_drops", $oldid, $newitemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this qtype to be processed by the links decoder
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function define_decode_contents() {
|
||||
|
||||
$prefix = 'qtype_'.self::qtype_name();
|
||||
|
||||
$contents = array();
|
||||
|
||||
$fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
|
||||
$contents[] =
|
||||
new restore_decode_content($prefix, $fields, $prefix);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="question/type/ddmarker/db" VERSION="20150914" COMMENT="XMLDB file for Moodle question/type/ddmarker."
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
<TABLES>
|
||||
<TABLE NAME="qtype_ddmarker" COMMENT="Defines drag and drop (text or images onto a background image) questions">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
|
||||
<FIELD NAME="correctfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any correct response."/>
|
||||
<FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="partiallycorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any partially correct response."/>
|
||||
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="incorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response."/>
|
||||
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="showmisplaced" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="qtype_ddmarker_drops" COMMENT="drop regions">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="no" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="drop number"/>
|
||||
<FIELD NAME="shape" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="circle, rectangle, polygon"/>
|
||||
<FIELD NAME="coords" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="choice" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="qtype_ddmarker_drags" COMMENT="Labels for markers to drag.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="no" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="drag no"/>
|
||||
<FIELD NAME="label" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Alt text label for drag-able image."/>
|
||||
<FIELD NAME="infinite" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="noofdrags" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="No of drag items, ignored if drag is infinite."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Ddmarker question type upgrade code.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2013 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Upgrade code for the ddmarker question type.
|
||||
* @param int $oldversion the version we are upgrading from.
|
||||
* @return bool
|
||||
*/
|
||||
function xmldb_qtype_ddmarker_upgrade($oldversion) {
|
||||
global $CFG, $DB;
|
||||
|
||||
$dbman = $DB->get_manager();
|
||||
|
||||
// Moodle v2.3.0 release upgrade line
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Moodle v2.4.0 release upgrade line
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Moodle v2.5.0 release upgrade line
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2013053000) {
|
||||
|
||||
// Define field noofdrags to be added to qtype_ddmarker_drags.
|
||||
$table = new xmldb_table('qtype_ddmarker_drags');
|
||||
$field = new xmldb_field('noofdrags', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '1', 'infinite');
|
||||
|
||||
// Conditionally launch add field noofdrags.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2013053000, 'qtype', 'ddmarker');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?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 the editing form for the drag-and-drop images onto images question type.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot.'/question/type/ddimageortext/edit_ddtoimage_form_base.php');
|
||||
require_once($CFG->dirroot.'/question/type/ddmarker/shapes.php');
|
||||
|
||||
define('QTYPE_DDMARKER_ALLOWED_TAGS_IN_MARKER', '<br><i><em><b><strong><sup><sub><u>');
|
||||
|
||||
|
||||
/**
|
||||
* Drag-and-drop images onto images editing form definition.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_edit_form extends qtype_ddtoimage_edit_form_base {
|
||||
public function qtype() {
|
||||
return 'ddmarker';
|
||||
}
|
||||
|
||||
protected function definition_inner($mform) {
|
||||
$mform->addElement('advcheckbox', 'showmisplaced', ' ',
|
||||
get_string('showmisplaced', 'qtype_ddmarker'));
|
||||
parent::definition_inner($mform);
|
||||
|
||||
$mform->addHelpButton('drops[0]', 'dropzones', 'qtype_ddmarker');
|
||||
}
|
||||
|
||||
public function js_call() {
|
||||
global $PAGE;
|
||||
$maxsizes = new stdClass();
|
||||
$maxsizes->bgimage = new stdClass();
|
||||
$maxsizes->bgimage->width = QTYPE_DDMARKER_BGIMAGE_MAXWIDTH;
|
||||
$maxsizes->bgimage->height = QTYPE_DDMARKER_BGIMAGE_MAXHEIGHT;
|
||||
|
||||
$params = array('maxsizes' => $maxsizes,
|
||||
'topnode' => 'fieldset#id_previewareaheader');
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddmarker-form',
|
||||
'M.qtype_ddmarker.init_form',
|
||||
array($params));
|
||||
}
|
||||
|
||||
|
||||
protected function definition_draggable_items($mform, $itemrepeatsatstart) {
|
||||
$mform->addElement('header', 'draggableitemheader',
|
||||
get_string('markers', 'qtype_ddmarker'));
|
||||
$mform->addElement('advcheckbox', 'shuffleanswers', ' ',
|
||||
get_string('shuffleimages', 'qtype_'.$this->qtype()));
|
||||
$mform->setDefault('shuffleanswers', 0);
|
||||
$this->repeat_elements($this->draggable_item($mform), $itemrepeatsatstart,
|
||||
$this->draggable_items_repeated_options(),
|
||||
'noitems', 'additems', self::ADD_NUM_ITEMS,
|
||||
get_string('addmoreitems', 'qtype_ddmarker'), true);
|
||||
}
|
||||
|
||||
protected function draggable_item($mform) {
|
||||
$draggableimageitem = array();
|
||||
|
||||
$grouparray = array();
|
||||
$grouparray[] = $mform->createElement('text', 'label', '',
|
||||
array('size' => 30, 'class' => 'tweakcss'));
|
||||
$mform->setType('text', PARAM_RAW_TRIMMED);
|
||||
|
||||
$noofdragoptions = array(0 => get_string('infinite', 'qtype_ddmarker'));
|
||||
foreach (range(1, 6) as $option) {
|
||||
$noofdragoptions[$option] = $option;
|
||||
}
|
||||
$grouparray[] = $mform->createElement('select', 'noofdrags', get_string('noofdrags', 'qtype_ddmarker'), $noofdragoptions);
|
||||
|
||||
$draggableimageitem[] = $mform->createElement('group', 'drags',
|
||||
get_string('marker_n', 'qtype_ddmarker'), $grouparray);
|
||||
return $draggableimageitem;
|
||||
}
|
||||
|
||||
protected function draggable_items_repeated_options() {
|
||||
$repeatedoptions = array();
|
||||
$repeatedoptions['drags[label]']['type'] = PARAM_RAW;
|
||||
return $repeatedoptions;
|
||||
}
|
||||
|
||||
protected function drop_zone($mform, $imagerepeats) {
|
||||
$dropzoneitem = array();
|
||||
|
||||
$grouparray = array();
|
||||
$shapearray = qtype_ddmarker_shape::shape_options();
|
||||
$grouparray[] = $mform->createElement('select', 'shape',
|
||||
get_string('shape', 'qtype_ddmarker'), $shapearray);
|
||||
$grouparray[] = $mform->createElement('text', 'coords',
|
||||
get_string('coords', 'qtype_ddmarker'),
|
||||
array('size' => 50, 'class' => 'tweakcss'));
|
||||
$mform->setType('coords', PARAM_RAW); // These are validated manually.
|
||||
$markernos = array();
|
||||
$markernos[0] = '';
|
||||
for ($i = 1; $i <= $imagerepeats; $i += 1) {
|
||||
$markernos[$i] = $i;
|
||||
}
|
||||
$grouparray[] = $mform->createElement('select', 'choice',
|
||||
get_string('marker', 'qtype_ddmarker'), $markernos);
|
||||
$dropzone = $mform->createElement('group', 'drops',
|
||||
get_string('dropzone', 'qtype_ddmarker', '{no}'), $grouparray);
|
||||
return array($dropzone);
|
||||
}
|
||||
|
||||
protected function drop_zones_repeated_options() {
|
||||
$repeatedoptions = array();
|
||||
$repeatedoptions['drops[coords]']['type'] = PARAM_RAW;
|
||||
return $repeatedoptions;
|
||||
}
|
||||
|
||||
protected function get_hint_fields($withclearwrong = false, $withshownumpartscorrect = false) {
|
||||
$mform = $this->_form;
|
||||
|
||||
$repeated = array();
|
||||
$repeated[] = $mform->createElement('editor', 'hint', get_string('hintn', 'question'),
|
||||
array('rows' => 5), $this->editoroptions);
|
||||
$repeatedoptions['hint']['type'] = PARAM_RAW;
|
||||
|
||||
$repeated[] = $mform->createElement('checkbox', 'hintshownumcorrect',
|
||||
get_string('options', 'question'),
|
||||
get_string('shownumpartscorrect', 'question'));
|
||||
$repeated[] = $mform->createElement('checkbox', 'hintoptions',
|
||||
'',
|
||||
get_string('stateincorrectlyplaced', 'qtype_ddmarker'));
|
||||
$repeated[] = $mform->createElement('checkbox', 'hintclearwrong',
|
||||
'',
|
||||
get_string('clearwrongparts', 'qtype_ddmarker'));
|
||||
|
||||
return array($repeated, $repeatedoptions);
|
||||
}
|
||||
|
||||
public function data_preprocessing($question) {
|
||||
|
||||
$question = parent::data_preprocessing($question);
|
||||
$question = $this->data_preprocessing_combined_feedback($question, true);
|
||||
$question = $this->data_preprocessing_hints($question, true, true);
|
||||
|
||||
$dragids = array(); // Drag no -> dragid.
|
||||
if (!empty($question->options)) {
|
||||
$question->shuffleanswers = $question->options->shuffleanswers;
|
||||
$question->showmisplaced = $question->options->showmisplaced;
|
||||
$question->drags = array();
|
||||
foreach ($question->options->drags as $drag) {
|
||||
$dragindex = $drag->no - 1;
|
||||
$question->drags[$dragindex] = array();
|
||||
$question->drags[$dragindex]['label'] = $drag->label;
|
||||
if ($drag->infinite == 1) {
|
||||
$question->drags[$dragindex]['noofdrags'] = 0;
|
||||
} else {
|
||||
$question->drags[$dragindex]['noofdrags'] = $drag->noofdrags;
|
||||
}
|
||||
$dragids[$dragindex] = $drag->id;
|
||||
}
|
||||
$question->drops = array();
|
||||
foreach ($question->options->drops as $drop) {
|
||||
$droparray = (array)$drop;
|
||||
unset($droparray['id']);
|
||||
unset($droparray['no']);
|
||||
unset($droparray['questionid']);
|
||||
$question->drops[$drop->no - 1] = $droparray;
|
||||
}
|
||||
}
|
||||
// Initialise file picker for bgimage.
|
||||
$draftitemid = file_get_submitted_draft_itemid('bgimage');
|
||||
|
||||
file_prepare_draft_area($draftitemid, $this->context->id, 'qtype_ddmarker',
|
||||
'bgimage', !empty($question->id) ? (int) $question->id : null,
|
||||
self::file_picker_options());
|
||||
$question->bgimage = $draftitemid;
|
||||
|
||||
$this->js_call();
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the necessary preprocessing for the hint fields.
|
||||
*
|
||||
* @param object $question The data being passed to the form.
|
||||
* @param bool $withclearwrong Clear wrong hints.
|
||||
* @param bool $withshownumpartscorrect Show number correct.
|
||||
* @return object The modified data.
|
||||
*/
|
||||
protected function data_preprocessing_hints($question, $withclearwrong = false,
|
||||
$withshownumpartscorrect = false) {
|
||||
if (empty($question->hints)) {
|
||||
return $question;
|
||||
}
|
||||
parent::data_preprocessing_hints($question, $withclearwrong, $withshownumpartscorrect);
|
||||
|
||||
$question->hintoptions = array();
|
||||
foreach ($question->hints as $hint) {
|
||||
$question->hintoptions[] = $hint->options;
|
||||
}
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
public function validation($data, $files) {
|
||||
$errors = parent::validation($data, $files);
|
||||
$bgimagesize = $this->get_image_size_in_draft_area($data['bgimage']);
|
||||
if ($bgimagesize === null) {
|
||||
$errors["bgimage"] = get_string('formerror_nobgimage', 'qtype_ddmarker');
|
||||
}
|
||||
|
||||
$allchoices = array();
|
||||
for ($i = 0; $i < $data['nodropzone']; $i++) {
|
||||
$choice = $data['drops'][$i]['choice'];
|
||||
$choicepresent = ($choice !== '0');
|
||||
|
||||
if ($choicepresent) {
|
||||
// Test coords here.
|
||||
if ($bgimagesize !== null) {
|
||||
$shape = $data['drops'][$i]['shape'];
|
||||
$coordsstring = $data['drops'][$i]['coords'];
|
||||
$shapeobj = qtype_ddmarker_shape::create($shape, $coordsstring);
|
||||
$interpretererror = $shapeobj->get_coords_interpreter_error();
|
||||
if ($interpretererror !== false) {
|
||||
$errors["drops[{$i}]"] = $interpretererror;
|
||||
} else if (!$shapeobj->inside_width_height($bgimagesize)) {
|
||||
$errorcode = 'shapeoutsideboundsofbgimage';
|
||||
$errors["drops[{$i}]"] =
|
||||
get_string('formerror_'.$errorcode, 'qtype_ddmarker');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (trim($data['drops'][$i]['coords']) !== '') {
|
||||
$errorcode = 'noitemselected';
|
||||
$errors["drops[{$i}]"] = get_string('formerror_'.$errorcode, 'qtype_ddmarker');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
for ($dragindex = 0; $dragindex < $data['noitems']; $dragindex++) {
|
||||
$label = $data['drags'][$dragindex]['label'];
|
||||
if ($label != strip_tags($label, QTYPE_DDMARKER_ALLOWED_TAGS_IN_MARKER)) {
|
||||
$errors["drags[{$dragindex}]"]
|
||||
= get_string('formerror_onlysometagsallowed', 'qtype_ddmarker',
|
||||
s(QTYPE_DDMARKER_ALLOWED_TAGS_IN_MARKER));
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width and height of a draft image.
|
||||
*
|
||||
* @param int $draftitemid ID of the draft image
|
||||
* @return array Return array of the width and height of the draft image.
|
||||
*/
|
||||
public function get_image_size_in_draft_area($draftitemid) {
|
||||
global $USER;
|
||||
$usercontext = context_user::instance($USER->id);
|
||||
$fs = get_file_storage();
|
||||
$draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
|
||||
if ($draftfiles) {
|
||||
foreach ($draftfiles as $file) {
|
||||
if ($file->is_directory()) {
|
||||
continue;
|
||||
}
|
||||
// Just return the data for the first good file, there should only be one.
|
||||
$imageinfo = $file->get_imageinfo();
|
||||
$width = $imageinfo['width'];
|
||||
$height = $imageinfo['height'];
|
||||
return array($width, $height);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Language strings for qtype_ddmarker.
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['addmoreitems'] = 'Blanks for {no} more markers';
|
||||
$string['alttext'] = 'Alt text';
|
||||
$string['answer'] = 'Answer';
|
||||
$string['bgimage'] = 'Background image';
|
||||
$string['coords'] = 'Coords';
|
||||
$string['correctansweris'] = 'The correct answer is: {$a}';
|
||||
$string['draggableimage'] = 'Draggable image';
|
||||
$string['draggableitem'] = 'Draggable item';
|
||||
$string['draggableitemheader'] = 'Draggable item {$a}';
|
||||
$string['draggableitemtype'] = 'Type';
|
||||
$string['draggableword'] = 'Draggable text';
|
||||
$string['dropbackground'] = 'Background image for dragging markers onto';
|
||||
$string['dropzone'] = 'Drop zone {$a}';
|
||||
$string['dropzoneheader'] = 'Drop zones';
|
||||
$string['dropzones'] = 'Drop zones';
|
||||
$string['dropzones_help'] = 'The drop zones are defined by typing co-ordinates. As you type, the preview above is immediately updated, so you can position things by trial and error.
|
||||
|
||||
* Circle: centre_x, centre_y; radius<br>for example: <code>80, 100; 50</code>
|
||||
* Polygon: x1, y1; x2, y2; ...; xn, yn<br>for example: <code>20, 60; 100, 60; 20, 100</code>
|
||||
* Rectangle: left, top, width, height<br>for example: <code>20, 60; 80, 40</code>';
|
||||
$string['followingarewrong'] = 'The following markers have been placed in the wrong area : {$a}.';
|
||||
$string['followingarewrongandhighlighted'] = 'The following markers were incorrectly placed : {$a}. Highlighted marker(s) are now shown with the correct placement(s).<br /> Click on the marker to highlight the allowed area.';
|
||||
$string['formerror_nobgimage'] = 'You need to select an image to use as the background for the drag and drop area.';
|
||||
$string['formerror_noitemselected'] = 'You have specified a drop zone but not chosen a marker that must be dragged to the zone';
|
||||
$string['formerror_nosemicolons'] = 'There are no semicolons in your coordinates string. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['formerror_onlysometagsallowed'] = 'Only "{$a}" tags are allowed in the label for a marker';
|
||||
$string['formerror_onlyusewholepositivenumbers'] = 'Please use only whole positive numbers to specify x,y coords and/or width and height of shapes. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['formerror_polygonmusthaveatleastthreepoints'] = 'For a polygon shape you need to specify at least 3 points. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['formerror_repeatedpoint'] = 'You have given the same point twice. Please remove the duplication. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['formerror_shapeoutsideboundsofbgimage'] = 'The shape you have defined goes out of the bounds of the background image';
|
||||
$string['formerror_toomanysemicolons'] = 'There are too many semi colon separated parts to the coordinates you have specified. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['formerror_unrecognisedwidthheightpart'] = 'We do not recognise the width and height you have specified. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['formerror_unrecognisedxypart'] = 'We do not recognise the x,y coordinates you have specified. Your coordinates for a {$a->shape} should be expressed as - {$a->coordsstring}.';
|
||||
$string['infinite'] = 'Infinite';
|
||||
$string['marker'] = 'Marker';
|
||||
$string['marker_n'] = 'Marker {no}';
|
||||
$string['markers'] = 'Markers';
|
||||
$string['nolabel'] = 'No label text';
|
||||
$string['noofdrags'] = 'Number';
|
||||
$string['pleasedragatleastonemarker'] = 'Your answer is not complete, you must place at least one marker on the image.';
|
||||
$string['pluginname'] = 'Drag and drop markers';
|
||||
$string['pluginname_help'] = 'select a background image file, enter text labels for markers and define the drop zones on the background image to which they must be dragged.';
|
||||
$string['pluginname_link'] = 'question/type/ddmarker';
|
||||
$string['pluginnameadding'] = 'Adding drag and drop markers';
|
||||
$string['pluginnameediting'] = 'Editing drag and drop markers';
|
||||
$string['pluginnamesummary'] = 'Markers are dragged and dropped onto a background image.';
|
||||
$string['previewareaheader'] = 'Preview';
|
||||
$string['previewareamessage'] = 'Select a background image file, enter text labels for markers and define the drop zones on the background image to which they must be dragged.';
|
||||
$string['refresh'] = 'Refresh preview';
|
||||
$string['clearwrongparts'] = 'Move incorrectly placed markers back to default start position below image';
|
||||
$string['shape'] = 'Shape';
|
||||
$string['shape_circle'] = 'Circle';
|
||||
$string['shape_circle_lowercase'] = 'circle';
|
||||
$string['shape_circle_coords'] = 'x,y;r (where x,y are the xy coordinates of the centre of the circle and r is the radius)';
|
||||
$string['shape_rectangle'] = 'Rectangle';
|
||||
$string['shape_rectangle_lowercase'] = 'rectangle';
|
||||
$string['shape_rectangle_coords'] = 'x,y;w,h (where x,y are the xy coordinates of the top left corner of the rectangle and w and h are the width and height of the rectangle)';
|
||||
$string['shape_polygon'] = 'Polygon';
|
||||
$string['shape_polygon_lowercase'] = 'polygon';
|
||||
$string['shape_polygon_coords'] = 'x1,y1;x2,y2;x3,y3;x4,y4....(where x1, y1 are the x,y coordinates of the first vertex, x2, y2 are the x,y coordinates of the second, etc. You do not need to repeat the coordinates for the first vertex to close the polygon)';
|
||||
$string['showmisplaced'] = 'Highlight drop zones which have not had the correct marker dropped on them';
|
||||
$string['shuffleimages'] = 'Shuffle drag items each time question is attempted';
|
||||
$string['stateincorrectlyplaced'] = 'State which markers are incorrectly placed';
|
||||
$string['summariseplace'] = '{$a->no}. {$a->text}';
|
||||
$string['summariseplaceno'] = 'Drop zone {$a}';
|
||||
$string['ytop'] = 'Top';
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Serve question type files.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Checks file access for ddmarker questions.
|
||||
*
|
||||
* @param object $course The course we are in
|
||||
* @param object $cm Course module
|
||||
* @param object $context The context object
|
||||
* @param string $filearea the name of the file area.
|
||||
* @param array $args the remaining bits of the file path.
|
||||
* @param bool $forcedownload whether the user must be forced to download the file.
|
||||
* @param array $options additional options affecting the file serving
|
||||
*/
|
||||
function qtype_ddmarker_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
question_pluginfile($course, $context, 'qtype_ddmarker', $filearea, $args, $forcedownload, $options);
|
||||
}
|
||||
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 539 B |
@@ -0,0 +1,507 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop markers question definition class.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/questionbase.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddmarker/shapes.php');
|
||||
|
||||
|
||||
/**
|
||||
* Represents a drag-and-drop markers question.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_question extends qtype_ddtoimage_question_base {
|
||||
|
||||
public $showmisplaced;
|
||||
|
||||
public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
|
||||
if ($filearea == 'bgimage') {
|
||||
$validfilearea = true;
|
||||
} else {
|
||||
$validfilearea = false;
|
||||
}
|
||||
if ($component == 'qtype_ddmarker' && $validfilearea) {
|
||||
$question = $qa->get_question();
|
||||
$itemid = reset($args);
|
||||
return $itemid == $question->id;
|
||||
} else {
|
||||
return parent::check_file_access($qa, $options, $component,
|
||||
$filearea, $args, $forcedownload);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get a choice identifier
|
||||
*
|
||||
* @param int $choice stem number
|
||||
* @return string the question-type variable name.
|
||||
*/
|
||||
public function choice($choice) {
|
||||
return 'c' . $choice;
|
||||
}
|
||||
|
||||
public function get_expected_data() {
|
||||
$vars = array();
|
||||
foreach ($this->choices[1] as $choice => $notused) {
|
||||
$vars[$this->choice($choice)] = PARAM_NOTAGS;
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
public function is_complete_response(array $response) {
|
||||
foreach ($this->choices[1] as $choiceno => $notused) {
|
||||
if (isset($response[$this->choice($choiceno)])
|
||||
&& '' != trim($response[$this->choice($choiceno)])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public function is_gradable_response(array $response) {
|
||||
return $this->is_complete_response($response);
|
||||
}
|
||||
public function is_same_response(array $prevresponse, array $newresponse) {
|
||||
foreach ($this->choices[1] as $choice => $notused) {
|
||||
$fieldname = $this->choice($choice);
|
||||
if (!$this->arrays_same_at_key_integer(
|
||||
$prevresponse, $newresponse, $fieldname)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Tests to see whether two arrays have the same set of coords at a particular key. Coords
|
||||
* can be in any order.
|
||||
* @param array $array1 the first array.
|
||||
* @param array $array2 the second array.
|
||||
* @param string $key an array key.
|
||||
* @return bool whether the two arrays have the same set of coords (or lack of them)
|
||||
* for a given key.
|
||||
*/
|
||||
public function arrays_same_at_key_integer(
|
||||
array $array1, array $array2, $key) {
|
||||
if (array_key_exists($key, $array1)) {
|
||||
$value1 = $array1[$key];
|
||||
} else {
|
||||
$value1 = '';
|
||||
}
|
||||
if (array_key_exists($key, $array2)) {
|
||||
$value2 = $array2[$key];
|
||||
} else {
|
||||
$value2 = '';
|
||||
}
|
||||
$coords1 = explode(';', $value1);
|
||||
$coords2 = explode(';', $value2);
|
||||
if (count($coords1) !== count($coords2)) {
|
||||
return false;
|
||||
} else if (count($coords1) === 0) {
|
||||
return true;
|
||||
} else {
|
||||
$valuesinbotharrays = $this->array_intersect_fixed($coords1, $coords2);
|
||||
return (count($valuesinbotharrays) == count($coords1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This function is a variation of array_intersect that checks for the existence of duplicate
|
||||
* array values too.
|
||||
* @author dml at nm dot ru (taken from comments on php manual)
|
||||
* @param array $array1
|
||||
* @param array $array2
|
||||
* @return bool whether array1 and array2 contain the same values including duplicate values
|
||||
*/
|
||||
protected function array_intersect_fixed($array1, $array2) {
|
||||
$result = array();
|
||||
foreach ($array1 as $val) {
|
||||
if (($key = array_search($val, $array2, true)) !== false) {
|
||||
$result[] = $val;
|
||||
unset($array2[$key]);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function get_validation_error(array $response) {
|
||||
if ($this->is_complete_response($response)) {
|
||||
return '';
|
||||
}
|
||||
return get_string('pleasedragatleastonemarker', 'qtype_ddmarker');
|
||||
}
|
||||
|
||||
public function get_num_parts_right(array $response) {
|
||||
$chosenhits = $this->choose_hits($response);
|
||||
$divisor = max(count($this->rightchoices), $this->total_number_of_items_dragged($response));
|
||||
return array(count($chosenhits), $divisor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose hits to maximize grade where drop targets may have more than one hit and drop targets
|
||||
* can overlap.
|
||||
* @param array $response
|
||||
* @return array chosen hits
|
||||
*/
|
||||
protected function choose_hits(array $response) {
|
||||
$allhits = $this->get_all_hits($response);
|
||||
$chosenhits = array();
|
||||
foreach ($allhits as $placeno => $hits) {
|
||||
foreach ($hits as $itemno => $hit) {
|
||||
$choice = $this->get_right_choice_for($placeno);
|
||||
$choiceitem = "$choice $itemno";
|
||||
if (!in_array($choiceitem, $chosenhits)) {
|
||||
$chosenhits[$placeno] = $choiceitem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $chosenhits;
|
||||
}
|
||||
public function total_number_of_items_dragged(array $response) {
|
||||
$total = 0;
|
||||
foreach ($this->choiceorder[1] as $choice) {
|
||||
$choicekey = $this->choice($choice);
|
||||
if (array_key_exists($choicekey, $response) && trim($response[$choicekey] !== '')) {
|
||||
$total += count(explode(';', $response[$choicekey]));
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's an array of all hits on drop targets. Needs further processing to find which hits
|
||||
* to select in the general case that drop targets may have more than one hit and drop targets
|
||||
* can overlap.
|
||||
* @param array $response
|
||||
* @return array all hits
|
||||
*/
|
||||
protected function get_all_hits(array $response) {
|
||||
$hits = array();
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
$rightchoice = $this->get_right_choice_for($placeno);
|
||||
$rightchoicekey = $this->choice($rightchoice);
|
||||
if (!array_key_exists($rightchoicekey, $response)) {
|
||||
continue;
|
||||
}
|
||||
$choicecoords = $response[$rightchoicekey];
|
||||
$coords = explode(';', $choicecoords);
|
||||
foreach ($coords as $itemno => $coord) {
|
||||
if (trim($coord) === '') {
|
||||
continue;
|
||||
}
|
||||
$pointxy = explode(',', $coord);
|
||||
if ($place->drop_hit($pointxy)) {
|
||||
if (!isset($hits[$placeno])) {
|
||||
$hits[$placeno] = array();
|
||||
}
|
||||
$hits[$placeno][$itemno] = $coord;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reverse sort in order of number of hits per place (if two or more
|
||||
// hits per place then we want to make sure hits do not hit elsewhere).
|
||||
$sortcomparison = function ($a1, $a2){
|
||||
return (count($a1) - count($a2));
|
||||
};
|
||||
uasort($hits, $sortcomparison);
|
||||
return $hits;
|
||||
}
|
||||
|
||||
public function get_right_choice_for($place) {
|
||||
$group = $this->places[$place]->group;
|
||||
foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
|
||||
if ($this->rightchoices[$place] == $choiceid) {
|
||||
return $choicekey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function grade_response(array $response) {
|
||||
list($right, $total) = $this->get_num_parts_right($response);
|
||||
$fraction = $right / $total;
|
||||
return array($fraction, question_state::graded_state_for_fraction($fraction));
|
||||
}
|
||||
|
||||
public function compute_final_grade($responses, $totaltries) {
|
||||
$maxitemsdragged = 0;
|
||||
$wrongtries = array();
|
||||
foreach ($responses as $i => $response) {
|
||||
$maxitemsdragged = max($maxitemsdragged,
|
||||
$this->total_number_of_items_dragged($response));
|
||||
$hits = $this->choose_hits($response);
|
||||
foreach ($hits as $place => $choiceitem) {
|
||||
if (!isset($wrongtries[$place])) {
|
||||
$wrongtries[$place] = $i;
|
||||
}
|
||||
}
|
||||
foreach ($wrongtries as $place => $notused) {
|
||||
if (!isset($hits[$place])) {
|
||||
unset($wrongtries[$place]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$numtries = count($responses);
|
||||
$numright = count($wrongtries);
|
||||
$penalty = array_sum($wrongtries) * $this->penalty;
|
||||
$grade = ($numright - $penalty) / (max($maxitemsdragged, count($this->places)));
|
||||
return $grade;
|
||||
}
|
||||
public function clear_wrong_from_response(array $response) {
|
||||
$hits = $this->choose_hits($response);
|
||||
|
||||
$cleanedresponse = array();
|
||||
foreach ($response as $choicekey => $coords) {
|
||||
$choice = (int)substr($choicekey, 1);
|
||||
$choiceresponse = array();
|
||||
$coordparts = explode(';', $coords);
|
||||
foreach ($coordparts as $itemno => $coord) {
|
||||
if (in_array("$choice $itemno", $hits)) {
|
||||
$choiceresponse[] = $coord;
|
||||
}
|
||||
}
|
||||
$cleanedresponse[$choicekey] = join(';', $choiceresponse);
|
||||
}
|
||||
return $cleanedresponse;
|
||||
}
|
||||
public function get_wrong_drags(array $response) {
|
||||
$hits = $this->choose_hits($response);
|
||||
$wrong = array();
|
||||
foreach ($response as $choicekey => $coords) {
|
||||
$choice = (int)substr($choicekey, 1);
|
||||
if ($coords != '') {
|
||||
$coordparts = explode(';', $coords);
|
||||
foreach ($coordparts as $itemno => $coord) {
|
||||
if (!in_array("$choice $itemno", $hits)) {
|
||||
$wrong[] = $this->get_selected_choice(1, $choice)->text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $wrong;
|
||||
}
|
||||
|
||||
|
||||
public function get_drop_zones_without_hit(array $response) {
|
||||
$hits = $this->choose_hits($response);
|
||||
|
||||
$nohits = array();
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
$choice = $this->get_right_choice_for($placeno);
|
||||
if (!isset($hits[$placeno])) {
|
||||
$nohit = new stdClass();
|
||||
$nohit->coords = $place->coords;
|
||||
$nohit->shape = $place->shape->name();
|
||||
$nohit->markertext = $this->choices[1][$this->choiceorder[1][$choice]]->text;
|
||||
$nohits[] = $nohit;
|
||||
}
|
||||
}
|
||||
return $nohits;
|
||||
}
|
||||
|
||||
public function classify_response(array $response) {
|
||||
$parts = array();
|
||||
$hits = $this->choose_hits($response);
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
if (isset($hits[$placeno])) {
|
||||
$shuffledchoiceno = $this->get_right_choice_for($placeno);
|
||||
$choice = $this->get_selected_choice(1, $shuffledchoiceno);
|
||||
$parts[$placeno] = new question_classified_response(
|
||||
$choice->no,
|
||||
$choice->summarise(),
|
||||
1 / count($this->places));
|
||||
} else {
|
||||
$parts[$placeno] = question_classified_response::no_response();
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public function get_correct_response() {
|
||||
$responsecoords = array();
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
$rightchoice = $this->get_right_choice_for($placeno);
|
||||
if ($rightchoice !== null) {
|
||||
$rightchoicekey = $this->choice($rightchoice);
|
||||
$correctcoords = $place->correct_coords();
|
||||
if ($correctcoords !== null) {
|
||||
if (!isset($responsecoords[$rightchoicekey])) {
|
||||
$responsecoords[$rightchoicekey] = array();
|
||||
}
|
||||
$responsecoords[$rightchoicekey][] = join(',', $correctcoords);
|
||||
}
|
||||
}
|
||||
}
|
||||
$response = array();
|
||||
foreach ($responsecoords as $choicekey => $coords) {
|
||||
$response[$choicekey] = join(';', $coords);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function get_right_answer_summary() {
|
||||
$placesummaries = array();
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
$shuffledchoiceno = $this->get_right_choice_for($placeno);
|
||||
$choice = $this->get_selected_choice(1, $shuffledchoiceno);
|
||||
$placesummaries[] = '{'.$place->summarise().' -> '.$choice->summarise().'}';
|
||||
}
|
||||
return join(', ', $placesummaries);
|
||||
}
|
||||
|
||||
public function summarise_response(array $response) {
|
||||
$hits = $this->choose_hits($response);
|
||||
$goodhits = array();
|
||||
foreach ($this->places as $placeno => $place) {
|
||||
if (isset($hits[$placeno])) {
|
||||
$shuffledchoiceno = $this->get_right_choice_for($placeno);
|
||||
$choice = $this->get_selected_choice(1, $shuffledchoiceno);
|
||||
$goodhits[] = "{".$place->summarise()." -> ". $choice->summarise(). "}";
|
||||
}
|
||||
}
|
||||
if (count($goodhits) == 0) {
|
||||
return null;
|
||||
}
|
||||
return implode(', ', $goodhits);
|
||||
}
|
||||
|
||||
public function get_random_guess_score() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one of the choices (draggable markers).
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_drag_item {
|
||||
/** @var string Label for the drag item */
|
||||
public $text;
|
||||
|
||||
/** @var int Number of the item */
|
||||
public $no;
|
||||
|
||||
/** @var int Group of the item */
|
||||
public $infinite;
|
||||
|
||||
/** @var int Number of drags */
|
||||
public $noofdrags;
|
||||
|
||||
/**
|
||||
* Drag item object setup.
|
||||
*
|
||||
* @param string $label The label text of the drag item
|
||||
* @param int $no Which number drag item this is
|
||||
* @param bool $infinite True if the item can be used an unlimited number of times
|
||||
* @param int $noofdrags
|
||||
*/
|
||||
public function __construct($label, $no, $infinite, $noofdrags) {
|
||||
$this->text = $label;
|
||||
$this->infinite = $infinite;
|
||||
$this->no = $no;
|
||||
$this->noofdrags = $noofdrags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the group of this item.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function choice_group() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates summary text of for the drag item.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function summarise() {
|
||||
return $this->text;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents one of the places (drop zones).
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_drop_zone {
|
||||
/** @var int Group of the item */
|
||||
public $group = 1;
|
||||
|
||||
/** @var int Number of the item */
|
||||
public $no;
|
||||
|
||||
/** @var object Shape of the item */
|
||||
public $shape;
|
||||
|
||||
/** @var array Location of the item */
|
||||
public $coords;
|
||||
|
||||
/**
|
||||
* Setup a drop zone object.
|
||||
*
|
||||
* @param int $no Which number drop zone this is
|
||||
* @param int $shape Shape of the drop zone
|
||||
* @param array $coords Coordinates of the zone
|
||||
*/
|
||||
public function __construct($no, $shape, $coords) {
|
||||
$this->no = $no;
|
||||
$this->shape = qtype_ddmarker_shape::create($shape, $coords);
|
||||
$this->coords = $coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates summary text of for the drop zone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function summarise() {
|
||||
return get_string('summariseplaceno', 'qtype_ddmarker', $this->no);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the it coordinates are in this drop zone.
|
||||
*
|
||||
* @param array $xy Array of X and Y location
|
||||
* @return bool
|
||||
*/
|
||||
public function drop_hit($xy) {
|
||||
return $this->shape->is_point_in_shape($xy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the center point of this zone
|
||||
*
|
||||
* @return array X and Y location
|
||||
*/
|
||||
public function correct_coords() {
|
||||
return $this->shape->center_point();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Question type class for the drag-and-drop images onto images question type.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/questiontypebase.php');
|
||||
|
||||
define('QTYPE_DDMARKER_BGIMAGE_MAXWIDTH', 600);
|
||||
define('QTYPE_DDMARKER_BGIMAGE_MAXHEIGHT', 400);
|
||||
|
||||
/**
|
||||
* Question hint for ddmarker.
|
||||
*
|
||||
* An extension of {@link question_hint} for questions like match and multiple
|
||||
* choice with multile answers, where there are options for whether to show the
|
||||
* number of parts right at each stage, and to reset the wrong parts.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_hint_ddmarker extends question_hint_with_parts {
|
||||
|
||||
public $statewhichincorrect;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param int the hint id from the database.
|
||||
* @param string $hint The hint text
|
||||
* @param int the corresponding text FORMAT_... type.
|
||||
* @param bool $shownumcorrect whether the number of right parts should be shown
|
||||
* @param bool $clearwrong whether the wrong parts should be reset.
|
||||
*/
|
||||
public function __construct($id, $hint, $hintformat, $shownumcorrect,
|
||||
$clearwrong, $statewhichincorrect) {
|
||||
parent::__construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong);
|
||||
$this->statewhichincorrect = $statewhichincorrect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic hint from a row loaded from the question_hints table in the database.
|
||||
* @param object $row with property options as well as hint, shownumcorrect and clearwrong set.
|
||||
* @return question_hint_ddmarker
|
||||
*/
|
||||
public static function load_from_record($row) {
|
||||
return new question_hint_ddmarker($row->id, $row->hint, $row->hintformat,
|
||||
$row->shownumcorrect, $row->clearwrong, $row->options);
|
||||
}
|
||||
|
||||
public function adjust_display_options(question_display_options $options) {
|
||||
parent::adjust_display_options($options);
|
||||
$options->statewhichincorrect = $this->statewhichincorrect;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The drag-and-drop markers question type class.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker extends qtype_ddtoimage_base {
|
||||
|
||||
public function save_question_options($formdata) {
|
||||
global $DB, $USER;
|
||||
$context = $formdata->context;
|
||||
|
||||
$options = $DB->get_record('qtype_ddmarker', array('questionid' => $formdata->id));
|
||||
if (!$options) {
|
||||
$options = new stdClass();
|
||||
$options->questionid = $formdata->id;
|
||||
$options->correctfeedback = '';
|
||||
$options->partiallycorrectfeedback = '';
|
||||
$options->incorrectfeedback = '';
|
||||
$options->id = $DB->insert_record('qtype_ddmarker', $options);
|
||||
}
|
||||
|
||||
$options->shuffleanswers = !empty($formdata->shuffleanswers);
|
||||
$options->showmisplaced = !empty($formdata->showmisplaced);
|
||||
$options = $this->save_combined_feedback_helper($options, $formdata, $context, true);
|
||||
$this->save_hints($formdata, true);
|
||||
$DB->update_record('qtype_ddmarker', $options);
|
||||
$DB->delete_records('qtype_ddmarker_drops', array('questionid' => $formdata->id));
|
||||
foreach (array_keys($formdata->drops) as $dropno) {
|
||||
if ($formdata->drops[$dropno]['choice'] == 0) {
|
||||
continue;
|
||||
}
|
||||
$drop = new stdClass();
|
||||
$drop->questionid = $formdata->id;
|
||||
$drop->no = $dropno + 1;
|
||||
$drop->shape = $formdata->drops[$dropno]['shape'];
|
||||
$drop->coords = $formdata->drops[$dropno]['coords'];
|
||||
$drop->choice = $formdata->drops[$dropno]['choice'];
|
||||
|
||||
$DB->insert_record('qtype_ddmarker_drops', $drop);
|
||||
}
|
||||
|
||||
// An array of drag no -> drag id.
|
||||
$olddragids = $DB->get_records_menu('qtype_ddmarker_drags',
|
||||
array('questionid' => $formdata->id),
|
||||
'', 'no, id');
|
||||
foreach (array_keys($formdata->drags) as $dragno) {
|
||||
if (!empty($formdata->drags[$dragno]['label'])) {
|
||||
$drag = new stdClass();
|
||||
$drag->questionid = $formdata->id;
|
||||
$drag->no = $dragno + 1;
|
||||
if ($formdata->drags[$dragno]['noofdrags'] == 0) {
|
||||
$drag->infinite = 1;
|
||||
$drag->noofdrags = 1;
|
||||
} else {
|
||||
$drag->infinite = 0;
|
||||
$drag->noofdrags = $formdata->drags[$dragno]['noofdrags'];
|
||||
}
|
||||
$drag->label = $formdata->drags[$dragno]['label'];
|
||||
|
||||
if (isset($olddragids[$dragno + 1])) {
|
||||
$drag->id = $olddragids[$dragno + 1];
|
||||
unset($olddragids[$dragno + 1]);
|
||||
$DB->update_record('qtype_ddmarker_drags', $drag);
|
||||
} else {
|
||||
$drag->id = $DB->insert_record('qtype_ddmarker_drags', $drag);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if (!empty($olddragids)) {
|
||||
list($sql, $params) = $DB->get_in_or_equal(array_values($olddragids));
|
||||
$DB->delete_records_select('qtype_ddmarker_drags', "id $sql", $params);
|
||||
}
|
||||
|
||||
self::constrain_image_size_in_draft_area($formdata->bgimage,
|
||||
QTYPE_DDMARKER_BGIMAGE_MAXWIDTH,
|
||||
QTYPE_DDMARKER_BGIMAGE_MAXHEIGHT);
|
||||
file_save_draft_area_files($formdata->bgimage, $formdata->context->id,
|
||||
'qtype_ddmarker', 'bgimage', $formdata->id,
|
||||
array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
|
||||
}
|
||||
|
||||
public function save_hints($formdata, $withparts = false) {
|
||||
global $DB;
|
||||
$context = $formdata->context;
|
||||
|
||||
$oldhints = $DB->get_records('question_hints',
|
||||
array('questionid' => $formdata->id), 'id ASC');
|
||||
|
||||
if (!empty($formdata->hint)) {
|
||||
$numhints = max(array_keys($formdata->hint)) + 1;
|
||||
} else {
|
||||
$numhints = 0;
|
||||
}
|
||||
|
||||
if ($withparts) {
|
||||
if (!empty($formdata->hintclearwrong)) {
|
||||
$numclears = max(array_keys($formdata->hintclearwrong)) + 1;
|
||||
} else {
|
||||
$numclears = 0;
|
||||
}
|
||||
if (!empty($formdata->hintshownumcorrect)) {
|
||||
$numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
|
||||
} else {
|
||||
$numshows = 0;
|
||||
}
|
||||
$numhints = max($numhints, $numclears, $numshows);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $numhints; $i += 1) {
|
||||
if (html_is_blank($formdata->hint[$i]['text'])) {
|
||||
$formdata->hint[$i]['text'] = '';
|
||||
}
|
||||
|
||||
if ($withparts) {
|
||||
$clearwrong = !empty($formdata->hintclearwrong[$i]);
|
||||
$shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
|
||||
$statewhichincorrect = !empty($formdata->hintoptions[$i]);
|
||||
}
|
||||
|
||||
if (empty($formdata->hint[$i]['text']) && empty($clearwrong) &&
|
||||
empty($shownumcorrect) && empty($statewhichincorrect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update an existing hint if possible.
|
||||
$hint = array_shift($oldhints);
|
||||
if (!$hint) {
|
||||
$hint = new stdClass();
|
||||
$hint->questionid = $formdata->id;
|
||||
$hint->hint = '';
|
||||
$hint->id = $DB->insert_record('question_hints', $hint);
|
||||
}
|
||||
|
||||
$hint->hint = $this->import_or_save_files($formdata->hint[$i],
|
||||
$context, 'question', 'hint', $hint->id);
|
||||
$hint->hintformat = $formdata->hint[$i]['format'];
|
||||
if ($withparts) {
|
||||
$hint->clearwrong = $clearwrong;
|
||||
$hint->shownumcorrect = $shownumcorrect;
|
||||
$hint->options = $statewhichincorrect;
|
||||
}
|
||||
$DB->update_record('question_hints', $hint);
|
||||
}
|
||||
|
||||
// Delete any remaining old hints.
|
||||
$fs = get_file_storage();
|
||||
foreach ($oldhints as $oldhint) {
|
||||
$fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
|
||||
$DB->delete_records('question_hints', array('id' => $oldhint->id));
|
||||
}
|
||||
}
|
||||
|
||||
protected function make_hint($hint) {
|
||||
return question_hint_ddmarker::load_from_record($hint);
|
||||
}
|
||||
protected function make_choice($dragdata) {
|
||||
return new qtype_ddmarker_drag_item($dragdata->label, $dragdata->no, $dragdata->infinite, $dragdata->noofdrags);
|
||||
}
|
||||
|
||||
protected function make_place($dropdata) {
|
||||
return new qtype_ddmarker_drop_zone($dropdata->no, $dropdata->shape, $dropdata->coords);
|
||||
}
|
||||
|
||||
protected function initialise_combined_feedback(question_definition $question,
|
||||
$questiondata, $withparts = false) {
|
||||
parent::initialise_combined_feedback($question, $questiondata, $withparts);
|
||||
$question->showmisplaced = $questiondata->options->showmisplaced;
|
||||
}
|
||||
|
||||
public function move_files($questionid, $oldcontextid, $newcontextid) {
|
||||
global $DB;
|
||||
$fs = get_file_storage();
|
||||
|
||||
parent::move_files($questionid, $oldcontextid, $newcontextid);
|
||||
$fs->move_area_files_to_new_context($oldcontextid,
|
||||
$newcontextid, 'qtype_ddmarker', 'bgimage', $questionid);
|
||||
|
||||
$this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
|
||||
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the files belonging to this question.
|
||||
* @param int $questionid the question being deleted.
|
||||
* @param int $contextid the context the question is in.
|
||||
*/
|
||||
|
||||
protected function delete_files($questionid, $contextid) {
|
||||
global $DB;
|
||||
$fs = get_file_storage();
|
||||
|
||||
parent::delete_files($questionid, $contextid);
|
||||
|
||||
$this->delete_files_in_combined_feedback($questionid, $contextid);
|
||||
$this->delete_files_in_hints($questionid, $contextid);
|
||||
}
|
||||
|
||||
public function export_to_xml($question, qformat_xml $format, $extra = null) {
|
||||
$fs = get_file_storage();
|
||||
$contextid = $question->contextid;
|
||||
$output = '';
|
||||
|
||||
if ($question->options->shuffleanswers) {
|
||||
$output .= " <shuffleanswers/>\n";
|
||||
}
|
||||
if ($question->options->showmisplaced) {
|
||||
$output .= " <showmisplaced/>\n";
|
||||
}
|
||||
$output .= $format->write_combined_feedback($question->options,
|
||||
$question->id,
|
||||
$question->contextid);
|
||||
$files = $fs->get_area_files($contextid, 'qtype_ddmarker', 'bgimage', $question->id);
|
||||
$output .= " ".$this->write_files($files, 2)."\n";;
|
||||
|
||||
foreach ($question->options->drags as $drag) {
|
||||
$files =
|
||||
$fs->get_area_files($contextid, 'qtype_ddmarker', 'dragimage', $drag->id);
|
||||
$output .= " <drag>\n";
|
||||
$output .= " <no>{$drag->no}</no>\n";
|
||||
$output .= $format->writetext($drag->label, 3);
|
||||
if ($drag->infinite) {
|
||||
$output .= " <infinite/>\n";
|
||||
}
|
||||
$output .= " <noofdrags>{$drag->noofdrags}</noofdrags>\n";
|
||||
$output .= " </drag>\n";
|
||||
}
|
||||
foreach ($question->options->drops as $drop) {
|
||||
$output .= " <drop>\n";
|
||||
$output .= " <no>{$drop->no}</no>\n";
|
||||
$output .= " <shape>{$drop->shape}</shape>\n";
|
||||
$output .= " <coords>{$drop->coords}</coords>\n";
|
||||
$output .= " <choice>{$drop->choice}</choice>\n";
|
||||
$output .= " </drop>\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
|
||||
if (!isset($data['@']['type']) || $data['@']['type'] != 'ddmarker') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$question = $format->import_headers($data);
|
||||
$question->qtype = 'ddmarker';
|
||||
|
||||
$question->shuffleanswers = array_key_exists('shuffleanswers',
|
||||
$format->getpath($data, array('#'), array()));
|
||||
$question->showmisplaced = array_key_exists('showmisplaced',
|
||||
$format->getpath($data, array('#'), array()));
|
||||
|
||||
$filexml = $format->getpath($data, array('#', 'file'), array());
|
||||
$question->bgimage = $format->import_files_as_draft($filexml);
|
||||
$drags = $data['#']['drag'];
|
||||
$question->drags = array();
|
||||
|
||||
foreach ($drags as $dragxml) {
|
||||
$dragno = $format->getpath($dragxml, array('#', 'no', 0, '#'), 0);
|
||||
$dragindex = $dragno - 1;
|
||||
$question->drags[$dragindex] = array();
|
||||
$question->drags[$dragindex]['label'] =
|
||||
$format->getpath($dragxml, array('#', 'text', 0, '#'), '', true);
|
||||
if (array_key_exists('infinite', $dragxml['#'])) {
|
||||
$question->drags[$dragindex]['noofdrags'] = 0; // Means infinite in the form.
|
||||
} else {
|
||||
// Defaults to 1 if 'noofdrags' not set.
|
||||
$question->drags[$dragindex]['noofdrags'] = $format->getpath($dragxml, array('#', 'noofdrags', 0, '#'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
$drops = $data['#']['drop'];
|
||||
$question->drops = array();
|
||||
foreach ($drops as $dropxml) {
|
||||
$dropno = $format->getpath($dropxml, array('#', 'no', 0, '#'), 0);
|
||||
$dropindex = $dropno - 1;
|
||||
$question->drops[$dropindex] = array();
|
||||
$question->drops[$dropindex]['choice'] =
|
||||
$format->getpath($dropxml, array('#', 'choice', 0, '#'), 0);
|
||||
$question->drops[$dropindex]['shape'] =
|
||||
$format->getpath($dropxml, array('#', 'shape', 0, '#'), '');
|
||||
$question->drops[$dropindex]['coords'] =
|
||||
$format->getpath($dropxml, array('#', 'coords', 0, '#'), '');
|
||||
}
|
||||
|
||||
$format->import_combined_feedback($question, $data, true);
|
||||
$format->import_hints($question, $data, true, true,
|
||||
$format->get_format($question->questiontextformat));
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
public function get_random_guess_score($questiondata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop markers question renderer class.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/rendererbase.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddimageortext/rendererbase.php');
|
||||
|
||||
|
||||
/**
|
||||
* Generates the output for drag-and-drop markers questions.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_renderer extends qtype_ddtoimage_renderer_base {
|
||||
public function formulation_and_controls(question_attempt $qa,
|
||||
question_display_options $options) {
|
||||
global $PAGE, $OUTPUT;
|
||||
|
||||
$question = $qa->get_question();
|
||||
$response = $qa->get_last_qt_data();
|
||||
|
||||
$questiontext = $question->format_questiontext($qa);
|
||||
|
||||
$output = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
|
||||
|
||||
$bgimage = self::get_url_for_image($qa, 'bgimage');
|
||||
|
||||
$img = html_writer::empty_tag('img', array(
|
||||
'src' => $bgimage, 'class' => 'dropbackground',
|
||||
'alt' => get_string('dropbackground', 'qtype_ddmarker')));
|
||||
|
||||
$droparea = html_writer::tag('div', $img, array('class' => 'droparea'));
|
||||
|
||||
$draghomes = '';
|
||||
$orderedgroup = $question->get_ordered_choices(1);
|
||||
$componentname = $question->qtype->plugin_name();
|
||||
$hiddenfields = '';
|
||||
foreach ($orderedgroup as $choiceno => $drag) {
|
||||
$classes = array('draghome',
|
||||
"choice{$choiceno}");
|
||||
if ($drag->infinite) {
|
||||
$classes[] = 'infinite';
|
||||
} else {
|
||||
$classes[] = 'dragno'.$drag->noofdrags;
|
||||
}
|
||||
$targeticonhtml =
|
||||
$OUTPUT->pix_icon('crosshairs', '', $componentname, array('class' => 'target'));
|
||||
|
||||
$markertextattrs = array('class' => 'markertext');
|
||||
$markertext = html_writer::tag('span', $drag->text, $markertextattrs);
|
||||
$draghomesattrs = array('class' => join(' ', $classes));
|
||||
$draghomes .= html_writer::tag('span', $targeticonhtml . $markertext, $draghomesattrs);
|
||||
$hiddenfields .= $this->hidden_field_choice($qa, $choiceno, $drag->infinite, $drag->noofdrags);
|
||||
}
|
||||
|
||||
$dragitemsclass = 'dragitems';
|
||||
if ($options->readonly) {
|
||||
$dragitemsclass .= ' readonly';
|
||||
}
|
||||
|
||||
$dragitems = html_writer::tag('div', $draghomes, array('class' => $dragitemsclass));
|
||||
$dropzones = html_writer::tag('div', '', array('class' => 'dropzones'));
|
||||
$texts = html_writer::tag('div', '', array('class' => 'markertexts'));
|
||||
$output .= html_writer::tag('div',
|
||||
$droparea.$dragitems.$dropzones . $texts,
|
||||
array('class' => 'ddarea'));
|
||||
|
||||
if ($question->showmisplaced && $qa->get_state()->is_finished()) {
|
||||
$visibledropzones = $question->get_drop_zones_without_hit($response);
|
||||
} else {
|
||||
$visibledropzones = array();
|
||||
}
|
||||
|
||||
$topnode = 'div#q'.$qa->get_slot();
|
||||
$params = array('dropzones' => $visibledropzones,
|
||||
'topnode' => $topnode,
|
||||
'readonly' => $options->readonly);
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddmarker-dd',
|
||||
'M.qtype_ddmarker.init_question',
|
||||
array($params));
|
||||
|
||||
if ($qa->get_state() == question_state::$invalid) {
|
||||
$output .= html_writer::nonempty_tag('div',
|
||||
$question->get_validation_error($qa->get_last_qt_data()),
|
||||
array('class' => 'validationerror'));
|
||||
}
|
||||
|
||||
if ($question->showmisplaced && $qa->get_state()->is_finished()) {
|
||||
$wrongparts = $question->get_drop_zones_without_hit($response);
|
||||
if (count($wrongparts) !== 0) {
|
||||
$wrongpartsstringspans = array();
|
||||
foreach ($wrongparts as $wrongpart) {
|
||||
$wrongpartsstringspans[] = html_writer::nonempty_tag('span',
|
||||
$wrongpart->markertext, array('class' => 'wrongpart'));
|
||||
}
|
||||
$wrongpartsstring = join(', ', $wrongpartsstringspans);
|
||||
$output .= html_writer::nonempty_tag('span',
|
||||
get_string('followingarewrongandhighlighted',
|
||||
'qtype_ddmarker',
|
||||
$wrongpartsstring),
|
||||
array('class' => 'wrongparts'));
|
||||
}
|
||||
}
|
||||
|
||||
$output .= html_writer::tag('div', $hiddenfields, array('class' => 'ddform'));
|
||||
return $output;
|
||||
}
|
||||
protected function hidden_field_choice(question_attempt $qa, $choiceno, $infinite, $noofdrags, $value = null) {
|
||||
$varname = 'c'.$choiceno;
|
||||
$classes = array('choices', 'choice'.$choiceno, 'noofdrags'.$noofdrags);
|
||||
if ($infinite) {
|
||||
$classes[] = 'infinite';
|
||||
}
|
||||
list(, $html) = $this->hidden_field_for_qt_var($qa, $varname, null, $classes);
|
||||
return $html;
|
||||
}
|
||||
|
||||
protected function hint(question_attempt $qa, question_hint $hint) {
|
||||
$output = '';
|
||||
$question = $qa->get_question();
|
||||
$response = $qa->get_last_qt_data();
|
||||
if ($hint->statewhichincorrect) {
|
||||
$wrongdrags = $question->get_wrong_drags($response);
|
||||
$wrongparts = array();
|
||||
foreach ($wrongdrags as $wrongdrag) {
|
||||
$wrongparts[] = html_writer::nonempty_tag('span',
|
||||
$wrongdrag, array('class' => 'wrongpart'));
|
||||
}
|
||||
$output .= html_writer::nonempty_tag('div',
|
||||
get_string('followingarewrong', 'qtype_ddmarker', join(', ', $wrongparts)),
|
||||
array('class' => 'wrongparts'));
|
||||
}
|
||||
$output .= parent::hint($qa, $hint);
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop markers classes for dealing with shapes on the server side.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Base class to represent a shape.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class qtype_ddmarker_shape {
|
||||
/** @var bool Indicates if there is an error */
|
||||
protected $error = false;
|
||||
|
||||
/** @var string The shape class prefix */
|
||||
protected static $classnameprefix = 'qtype_ddmarker_shape_';
|
||||
|
||||
public function __construct($coordsstring) {
|
||||
|
||||
}
|
||||
public function inside_width_height($widthheight) {
|
||||
foreach ($this->outlying_coords_to_test() as $coordsxy) {
|
||||
if ($coordsxy[0] > $widthheight[0] || $coordsxy[1] > $widthheight[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract protected function outlying_coords_to_test();
|
||||
|
||||
/**
|
||||
* Returns the center location of the shape.
|
||||
*
|
||||
* @return array X and Y location
|
||||
*/
|
||||
abstract public function center_point();
|
||||
|
||||
/**
|
||||
* Test if all passed parameters consist of only numbers.
|
||||
*
|
||||
* @return bool True if only numbers
|
||||
*/
|
||||
protected function is_only_numbers() {
|
||||
$args = func_get_args();
|
||||
foreach ($args as $arg) {
|
||||
if (0 === preg_match('!^[0-9]+$!', $arg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the point is within the bounding box made by top left and bottom right
|
||||
*
|
||||
* @param array $pointxy Array of the point (x, y)
|
||||
* @param array $xleftytop Top left point of bounding box
|
||||
* @param array $xrightybottom Bottom left point of bounding box
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_point_in_bounding_box($pointxy, $xleftytop, $xrightybottom) {
|
||||
if ($pointxy[0] <= $xleftytop[0]) {
|
||||
return false;
|
||||
} else if ($pointxy[0] >= $xrightybottom[0]) {
|
||||
return false;
|
||||
} else if ($pointxy[1] <= $xleftytop[1]) {
|
||||
return false;
|
||||
} else if ($pointxy[1] >= $xrightybottom[1]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any coordinate error
|
||||
*
|
||||
* @return string|bool String of the error or false if there is no error
|
||||
*/
|
||||
public function get_coords_interpreter_error() {
|
||||
if ($this->error) {
|
||||
$a = new stdClass();
|
||||
$a->shape = self::human_readable_name(true);
|
||||
$a->coordsstring = self::human_readable_coords_format();
|
||||
return get_string('formerror_'.$this->error, 'qtype_ddmarker', $a);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the location is within the shape.
|
||||
*
|
||||
* @param array $xy $xy[0] is x, $xy[1] is y
|
||||
* @return boolean is point inside shape
|
||||
*/
|
||||
abstract public function is_point_in_shape($xy);
|
||||
|
||||
/**
|
||||
* Returns the name of the shape.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function name() {
|
||||
return substr(get_called_class(), strlen(self::$classnameprefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a human readable name of the shape.
|
||||
*
|
||||
* @param bool $lowercase True if it should be lowercase.
|
||||
* @return string
|
||||
*/
|
||||
public static function human_readable_name($lowercase = false) {
|
||||
$stringid = 'shape_'.self::name();
|
||||
if ($lowercase) {
|
||||
$stringid .= '_lowercase';
|
||||
}
|
||||
return get_string($stringid, 'qtype_ddmarker');
|
||||
}
|
||||
|
||||
public static function human_readable_coords_format() {
|
||||
return get_string('shape_'.self::name().'_coords', 'qtype_ddmarker');
|
||||
}
|
||||
|
||||
|
||||
public static function shape_options() {
|
||||
$grepexpression = '!^'.preg_quote(self::$classnameprefix, '!').'!';
|
||||
$shapes = preg_grep($grepexpression, get_declared_classes());
|
||||
$shapearray = array();
|
||||
foreach ($shapes as $shape) {
|
||||
$shapearray[$shape::name()] = $shape::human_readable_name();
|
||||
}
|
||||
asort($shapearray);
|
||||
return $shapearray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed shape exists.
|
||||
*
|
||||
* @param string $shape The shape name
|
||||
* @return bool
|
||||
*/
|
||||
public static function exists($shape) {
|
||||
return class_exists((self::$classnameprefix).$shape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new shape of the specified type.
|
||||
*
|
||||
* @param string $shape The shape to create
|
||||
* @param string $coordsstring The string describing the coordinates
|
||||
* @return object
|
||||
*/
|
||||
public static function create($shape, $coordsstring) {
|
||||
$classname = (self::$classnameprefix).$shape;
|
||||
return new $classname($coordsstring);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class to represent a rectangle.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_shape_rectangle extends qtype_ddmarker_shape {
|
||||
/** @var int Width of shape */
|
||||
protected $width;
|
||||
|
||||
/** @var int Height of shape */
|
||||
protected $height;
|
||||
|
||||
/** @var int Left location */
|
||||
protected $xleft;
|
||||
|
||||
/** @var int Top location */
|
||||
protected $ytop;
|
||||
|
||||
public function __construct($coordsstring) {
|
||||
$coordstring = preg_replace('!^\s*!', '', $coordsstring);
|
||||
$coordstring = preg_replace('!\s*$!', '', $coordsstring);
|
||||
$coordsstringparts = preg_split('!;!', $coordsstring);
|
||||
|
||||
if (count($coordsstringparts) > 2) {
|
||||
$this->error = 'toomanysemicolons';
|
||||
|
||||
} else if (count($coordsstringparts) < 2) {
|
||||
$this->error = 'nosemicolons';
|
||||
|
||||
} else {
|
||||
$xy = explode(',', $coordsstringparts[0]);
|
||||
$widthheightparts = explode(',', $coordsstringparts[1]);
|
||||
if (count($xy) !== 2) {
|
||||
$this->error = 'unrecognisedxypart';
|
||||
} else if (count($widthheightparts) !== 2) {
|
||||
$this->error = 'unrecognisedwidthheightpart';
|
||||
} else {
|
||||
$this->width = trim($widthheightparts[0]);
|
||||
$this->height = trim($widthheightparts[1]);
|
||||
$this->xleft = trim($xy[0]);
|
||||
$this->ytop = trim($xy[1]);
|
||||
}
|
||||
if (!$this->is_only_numbers($this->width, $this->height, $this->ytop, $this->xleft)) {
|
||||
$this->error = 'onlyusewholepositivenumbers';
|
||||
}
|
||||
$this->width = (int) $this->width;
|
||||
$this->height = (int) $this->height;
|
||||
$this->xleft = (int) $this->xleft;
|
||||
$this->ytop = (int) $this->ytop;
|
||||
}
|
||||
|
||||
}
|
||||
protected function outlying_coords_to_test() {
|
||||
return array($this->xleft + $this->width, $this->ytop + $this->height);
|
||||
}
|
||||
public function is_point_in_shape($xy) {
|
||||
return $this->is_point_in_bounding_box($xy, array($this->xleft, $this->ytop),
|
||||
array($this->xleft + $this->width, $this->ytop + $this->height));
|
||||
}
|
||||
public function center_point() {
|
||||
return array($this->xleft + round($this->width / 2),
|
||||
$this->ytop + round($this->height / 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class to represent a circle.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_shape_circle extends qtype_ddmarker_shape {
|
||||
/** @var int X center */
|
||||
protected $xcentre;
|
||||
|
||||
/** @var int Y center */
|
||||
protected $ycentre;
|
||||
|
||||
/** @var int Radius of circle */
|
||||
protected $radius;
|
||||
|
||||
public function __construct($coordsstring) {
|
||||
$coordstring = preg_replace('!\s!', '', $coordsstring);
|
||||
$coordsstringparts = explode(';', $coordsstring);
|
||||
|
||||
if (count($coordsstringparts) > 2) {
|
||||
$this->error = 'toomanysemicolons';
|
||||
|
||||
} else if (count($coordsstringparts) < 2) {
|
||||
$this->error = 'nosemicolons';
|
||||
|
||||
} else {
|
||||
$xy = explode(',', $coordsstringparts[0]);
|
||||
if (count($xy) !== 2) {
|
||||
$this->error = 'unrecognisedxypart';
|
||||
} else {
|
||||
$this->radius = trim($coordsstringparts[1]);
|
||||
$this->xcentre = trim($xy[0]);
|
||||
$this->ycentre = trim($xy[1]);
|
||||
}
|
||||
|
||||
if (!$this->is_only_numbers($this->xcentre, $this->ycentre, $this->radius)) {
|
||||
$this->error = 'onlyusewholepositivenumbers';
|
||||
}
|
||||
|
||||
$this->xcentre = (int) $this->xcentre;
|
||||
$this->ycentre = (int) $this->ycentre;
|
||||
$this->radius = (int) $this->radius;
|
||||
}
|
||||
}
|
||||
|
||||
protected function outlying_coords_to_test() {
|
||||
return array($this->xcentre + $this->radius, $this->ycentre + $this->radius);
|
||||
}
|
||||
|
||||
public function is_point_in_shape($xy) {
|
||||
$distancefromcentre = sqrt(pow(($xy[0] - $this->xcentre), 2) + pow(($xy[1] - $this->ycentre), 2));
|
||||
return $distancefromcentre < $this->radius;
|
||||
}
|
||||
|
||||
public function center_point() {
|
||||
return array($this->xcentre, $this->ycentre);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class to represent a polygon.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_shape_polygon extends qtype_ddmarker_shape {
|
||||
/**
|
||||
* @var array Arrary of xy coords where xy coords are also in a two element array [x,y].
|
||||
*/
|
||||
public $coords;
|
||||
/**
|
||||
* @var array min x and y coords in a two element array [x,y].
|
||||
*/
|
||||
protected $minxy;
|
||||
/**
|
||||
* @var array max x and y coords in a two element array [x,y].
|
||||
*/
|
||||
protected $maxxy;
|
||||
|
||||
public function __construct($coordsstring) {
|
||||
$this->coords = array();
|
||||
$coordstring = preg_replace('!\s!', '', $coordsstring);
|
||||
$coordsstringparts = explode(';', $coordsstring);
|
||||
if (count($coordsstringparts) < 3) {
|
||||
$this->error = 'polygonmusthaveatleastthreepoints';
|
||||
} else {
|
||||
$lastxy = null;
|
||||
foreach ($coordsstringparts as $coordsstringpart) {
|
||||
$xy = explode(',', $coordsstringpart);
|
||||
if (count($xy) !== 2) {
|
||||
$this->error = 'unrecognisedxypart';
|
||||
}
|
||||
if (!$this->is_only_numbers(trim($xy[0]), trim($xy[1]))) {
|
||||
$this->error = 'onlyusewholepositivenumbers';
|
||||
}
|
||||
$xy[0] = (int) $xy[0];
|
||||
$xy[1] = (int) $xy[1];
|
||||
if ($lastxy !== null && $lastxy[0] == $xy[0] && $lastxy[1] == $xy[1]) {
|
||||
$this->error = 'repeatedpoint';
|
||||
}
|
||||
$this->coords[] = $xy;
|
||||
$lastxy = $xy;
|
||||
if (isset($this->minxy)) {
|
||||
$this->minxy[0] = min($this->minxy[0], $xy[0]);
|
||||
$this->minxy[1] = min($this->minxy[1], $xy[1]);
|
||||
} else {
|
||||
$this->minxy[0] = $xy[0];
|
||||
$this->minxy[1] = $xy[1];
|
||||
}
|
||||
if (isset($this->maxxy)) {
|
||||
$this->maxxy[0] = max($this->maxxy[0], $xy[0]);
|
||||
$this->maxxy[1] = max($this->maxxy[1], $xy[1]);
|
||||
} else {
|
||||
$this->maxxy[0] = $xy[0];
|
||||
$this->maxxy[1] = $xy[1];
|
||||
}
|
||||
}
|
||||
// Make sure polygon is not closed.
|
||||
if ($this->coords[count($this->coords) - 1][0] == $this->coords[0][0] &&
|
||||
$this->coords[count($this->coords) - 1][1] == $this->coords[0][1]) {
|
||||
unset($this->coords[count($this->coords) - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function outlying_coords_to_test() {
|
||||
return array($this->minxy, $this->maxxy);
|
||||
}
|
||||
|
||||
public function is_point_in_shape($xy) {
|
||||
$pointatinfinity = new qtype_ddmarker_point(-1000000, $xy[1] + 1);
|
||||
$pointtotest = new qtype_ddmarker_point($xy[0], $xy[1]);
|
||||
$testsegment = new qtype_ddmarker_segment($pointatinfinity, $pointtotest);
|
||||
$windingnumber = 0;
|
||||
foreach ($this->coords as $index => $coord) {
|
||||
if ($index != 0) {
|
||||
$a = new qtype_ddmarker_point($this->coords[$index - 1][0],
|
||||
$this->coords[$index - 1][1]);
|
||||
} else {
|
||||
$a = new qtype_ddmarker_point($this->coords[count($this->coords) - 1][0],
|
||||
$this->coords[count($this->coords) - 1][1]);
|
||||
}
|
||||
$b = new qtype_ddmarker_point($this->coords[$index][0],
|
||||
$this->coords[$index][1]);
|
||||
$segment = new qtype_ddmarker_segment($a, $b);
|
||||
$intersects = $segment->intersects($testsegment);
|
||||
if ($intersects === null) {
|
||||
list($perturbedsegment, $testsegment) = $this->perturb($segment, $testsegment);
|
||||
if ($index !== 0) {
|
||||
$this->coords[$index - 1][0] = $perturbedsegment->a->x;
|
||||
$this->coords[$index - 1][1] = $perturbedsegment->a->y;
|
||||
} else {
|
||||
$this->coords[count($this->coords) - 1][0] = $perturbedsegment->a->x;
|
||||
$this->coords[count($this->coords) - 1][1] = $perturbedsegment->a->y;
|
||||
}
|
||||
$this->coords[$index][0] = $perturbedsegment->b->x;
|
||||
$this->coords[$index][1] = $perturbedsegment->b->y;
|
||||
$intersects = $perturbedsegment->intersects($testsegment);
|
||||
if ($intersects === null) {
|
||||
throw new coding_exception('Polygon hit test code failed '.
|
||||
'- Still touching end point after perturbation');
|
||||
} else if ($intersects) {
|
||||
$windingnumber++;
|
||||
}
|
||||
} else if ($intersects) {
|
||||
$windingnumber++;
|
||||
}
|
||||
}
|
||||
return ($windingnumber % 2) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* $v segment and this touch, move one of them slightly.
|
||||
* @param qtype_ddmarker_segment $v
|
||||
* @param int $ua
|
||||
* @param int $ub
|
||||
*/
|
||||
public function perturb($p, $q) {
|
||||
list(, $ua, $ub) = $p->intersection_point($q);
|
||||
$pt = 0.00001; // Perturbation factor.
|
||||
$h = $p->a->dist($p->b);
|
||||
if ($ua == 0) {
|
||||
// ... q1, q2 intersects p1 exactly, move vertex p1 closer to p2.
|
||||
$a = ($pt * $p->a->dist(new qtype_ddmarker_point($p->b->x, $p->a->y))) / $h;
|
||||
$b = ($pt * $p->b->dist(new qtype_ddmarker_point($p->b->x, $p->a->y))) / $h;
|
||||
$p->a->x = $p->a->x + $a;
|
||||
$p->a->y = $p->a->y + $b;
|
||||
} else if ($ua == 1) {
|
||||
// ... q1, q2 intersects p2 exactly, move vertex p2 closer to p1.
|
||||
$a = ($pt * $p->a->dist(new qtype_ddmarker_point($p->b->x, $p->a->y))) / $h;
|
||||
$b = ($pt * $p->b->dist(new qtype_ddmarker_point($p->b->x, $p->a->y))) / $h;
|
||||
$p->b->x = $p->b->x - $a;
|
||||
$p->b->y = $p->b->y - $b;
|
||||
} else if ($ub == 0) {
|
||||
// ... p1, p2 intersects q1 exactly, move vertex q1 closer to q2.
|
||||
$a = ($pt * $q->a->dist(new qtype_ddmarker_point($q->b->x, $q->a->y))) / $h;
|
||||
$b = ($pt * $q->b->dist(new qtype_ddmarker_point($q->b->x, $q->a->y))) / $h;
|
||||
$q->a->x = $q->a->x + $a;
|
||||
$q->a->y = $q->a->y + $b;
|
||||
} else if ($ub == 1) {
|
||||
// ... p1, p2 intersects q2 exactly, move vertex q2 closer to q1.
|
||||
$a = ($pt * $q->a->dist(new qtype_ddmarker_point($q->b->x, $q->a->y))) / $h;
|
||||
$b = ($pt * $q->b->dist(new qtype_ddmarker_point($q->b->x, $q->a->y))) / $h;
|
||||
$q->b->x = $q->b->x - $a;
|
||||
$q->b->y = $q->b->y - $b;
|
||||
}
|
||||
return array($p, $q);
|
||||
}
|
||||
public function center_point() {
|
||||
$center = array(round(($this->minxy[0] + $this->maxxy[0]) / 2),
|
||||
round(($this->minxy[1] + $this->maxxy[1]) / 2));
|
||||
if ($this->is_point_in_shape($center)) {
|
||||
return $center;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class to represent a point.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_point {
|
||||
/** @var int X location */
|
||||
public $x;
|
||||
|
||||
/** @var int Y location */
|
||||
public $y;
|
||||
public function __construct($x, $y) {
|
||||
$this->x = $x;
|
||||
$this->y = $y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the distance between this point and another
|
||||
*/
|
||||
public function dist($other) {
|
||||
return sqrt(pow($this->x - $other->x, 2) + pow($this->y - $other->y, 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a segment between two end points a and b.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_segment {
|
||||
/** @var object First point */
|
||||
public $a;
|
||||
|
||||
/** @var object Second point */
|
||||
public $b;
|
||||
|
||||
public function __construct(qtype_ddmarker_point $a, qtype_ddmarker_point $b) {
|
||||
$this->a = $a;
|
||||
$this->b = $b;
|
||||
}
|
||||
/**
|
||||
* Find if this segment intersects another segment $v.
|
||||
* @param segment $v
|
||||
* @return boolean does it intersect?
|
||||
*/
|
||||
public function intersects(qtype_ddmarker_segment $v) {
|
||||
// Algorithm from: http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/
|
||||
// $this is P1 to P2 and $v is P3 to P4.
|
||||
list($d, $ua, $ub) = $this->intersection_point($v);
|
||||
if ($d !== 0) { // The lines intersect at a point somewhere
|
||||
// The values of $ua and $ub tell us where the intersection occurred.
|
||||
if ( (($ua == 0 || $ua == 1 )&&($ub >= 0 && $ub <= 1))
|
||||
|| (($ub == 0 || $ub == 1) && ($ua >= 0 && $ua <= 1))) {
|
||||
// A value of exactly 0 or 1 means the intersection occurred right at the
|
||||
// start or end of the line segment. For our purposes we will consider this
|
||||
// NOT to be an intersection away from the intersecting line.
|
||||
// Degenerate case - segment exactly touches a line.
|
||||
return null;
|
||||
} else if (($ua > 0 && $ua < 1) && ($ub > 0 && $ub < 1)) {
|
||||
// A value between 0 and 1 means the intersection occurred within the
|
||||
// line segment.
|
||||
// Intersection occurs on both line segments.
|
||||
return true;
|
||||
} else {
|
||||
// The lines do not intersect within the line segments.
|
||||
return false;
|
||||
}
|
||||
} else { // The lines do not intersect.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function intersection_point(qtype_ddmarker_segment $v) {
|
||||
$d = (($v->b->y - $v->a->y) * ($this->b->x - $this->a->x)) -
|
||||
(($v->b->x - $v->a->x) * ($this->b->y - $this->a->y));
|
||||
if ($d != 0) { // The lines intersect at a point somewhere.
|
||||
$ua = (($v->b->x - $v->a->x) * ($this->a->y - $v->a->y) -
|
||||
($v->b->y - $v->a->y) * ($this->a->x - $v->a->x)) / $d;
|
||||
$ub = (($this->b->x - $this->a->x) * ($this->a->y - $v->a->y) -
|
||||
($this->b->y - $this->a->y) * ($this->a->x - $v->a->x)) / $d;
|
||||
} else {
|
||||
$ua = null;
|
||||
$ub = null;
|
||||
}
|
||||
return array($d, $ua, $ub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
.que.ddmarker .qtext {
|
||||
margin-bottom: 0.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.que.ddmarker div.droparea img, form.mform fieldset#id_previewareaheader div.droparea img {
|
||||
border: 1px solid #000;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.que.ddmarker .draghome img, .que.ddmarker .draghome span {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.que.ddmarker .dragitems .dragitem {
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
.que.ddmarker .dragitems .draghome {
|
||||
margin: 10px;
|
||||
}
|
||||
.que.ddmarker .dragitems {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.que.ddmarker .dragitems.readonly .dragitem {
|
||||
cursor: auto;
|
||||
}
|
||||
.que.ddmarker div.ddarea, form.mform fieldset#id_previewareaheader div.ddarea {
|
||||
text-align: center;
|
||||
}
|
||||
.que.ddmarker .dropbackground, form.mform fieldset#id_previewareaheader .dropbackground {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.que.ddmarker div.dragitems div.draghome, .que.ddmarker div.dragitems div.dragitem,
|
||||
form.mform fieldset#id_previewareaheader div.draghome, form.mform fieldset#id_previewareaheader div.drag {
|
||||
font: 13px/1.231 arial,helvetica,clean,sans-serif;
|
||||
}
|
||||
.que.ddmarker div.dragitems span.markertext,
|
||||
.que.ddmarker div.markertexts span.markertext,
|
||||
form.mform fieldset#id_previewareaheader div.markertexts span.markertext {
|
||||
margin: 5px;
|
||||
z-index: 3;
|
||||
background-color: white;
|
||||
border: 2px solid black;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.que.ddmarker div.markertexts span.markertext {
|
||||
z-index: 2;
|
||||
background-color: yellow;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: khaki;
|
||||
}
|
||||
.que.ddmarker span.wrongpart {
|
||||
background-color: yellow;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: khaki;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
filter: alpha(opacity=60);
|
||||
opacity: 0.6;
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.que.ddmarker div.dragitems img.target {
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: -3px;
|
||||
}
|
||||
.que.ddmarker div.dragitems div.draghome img.target {
|
||||
display: none;
|
||||
}
|
||||
.que.ddmarker .dragitem.yui3-dd-dragging span.markertext {
|
||||
z-index: 3;
|
||||
box-shadow: 3px 3px 4px #000;
|
||||
}
|
||||
#page-question-type-ddmarker .ddarea .grid {
|
||||
position: absolute;
|
||||
background: url([[pix:qtype_ddmarker|grid]]) repeat scroll 0 0;
|
||||
}
|
||||
/* Editing form. Style repeated elements*/
|
||||
/*Top*/
|
||||
body#page-question-type-ddmarker div[id^=fitem_id_][id*=hint_] {
|
||||
background: #EEE;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid #BBB;
|
||||
border-bottom: 0;
|
||||
}
|
||||
body#page-question-type-ddmarker div[id^=fitem_id_][id*=hint_] .fitemtitle {
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Middle */
|
||||
body#page-question-type-ddmarker div[id^=fitem_id_][id*=hintoptions_],
|
||||
body#page-question-type-ddmarker div[id^=fitem_id_][id*=hintshownumcorrect_] {
|
||||
background: #EEE;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid #BBB;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
/* Bottom */
|
||||
body#page-question-type-ddmarker div[id^=fitem_id_][id*=hintclearwrong_] {
|
||||
background: #EEE;
|
||||
margin-bottom: 2em;
|
||||
margin-top: 0;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid #BBB;
|
||||
border-top: 0;
|
||||
}
|
||||
body#page-question-type-ddmarker #fitem_id_penalty {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@qtype @qtype_ddmarker
|
||||
Feature: Test creating a drag and drop markers question
|
||||
As a teacher
|
||||
In order to test my students
|
||||
I need to be able to create drag and drop markers questions
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a drag and drop markers question
|
||||
When I press "Create a new question ..."
|
||||
And I set the field "Drag and drop markers" to "1"
|
||||
And I press "Add"
|
||||
And I set the field "Question name" to "Drag and drop markers"
|
||||
And I set the field "Question text" to "Please place the markers on the map of Milton Keynes and be aware that there is more than one railway station."
|
||||
And I set the field "General feedback" to "The Open University is at the junction of Brickhill Street and Groveway. There are three railway stations, Wolverton, Milton Keynes Central and Bletchley."
|
||||
And I upload "question/type/ddmarker/tests/fixtures/mkmap.png" file to "Background image" filemanager
|
||||
|
||||
# Markers.
|
||||
And I follow "Markers"
|
||||
And I set the field "id_drags_0_label" to "OU"
|
||||
And I set the field "id_drags_0_noofdrags" to "1"
|
||||
And I set the field "id_drags_1_label" to "Railway station"
|
||||
And I set the field "id_drags_1_noofdrags" to "3"
|
||||
|
||||
# Drop zones.
|
||||
And I follow "Drop zones"
|
||||
And I set the field "id_drops_0_shape" to "Circle"
|
||||
And I set the field "id_drops_0_coords" to "322,213;10"
|
||||
And I set the field "id_drops_0_choice" to "1"
|
||||
And I set the field "id_drops_1_shape" to "Circle"
|
||||
And I set the field "id_drops_1_coords" to "144,84;10"
|
||||
And I set the field "id_drops_1_choice" to "2"
|
||||
And I set the field "id_drops_2_shape" to "Circle"
|
||||
And I set the field "id_drops_2_coords" to "195,180;10"
|
||||
And I set the field "id_drops_2_choice" to "2"
|
||||
And I set the field "id_drops_3_shape" to "Circle"
|
||||
And I set the field "id_drops_3_coords" to "267,302;10"
|
||||
|
||||
# Try to submit without setting the last marker.
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "You have specified a drop zone but not chosen a marker that must be dragged to the zone"
|
||||
|
||||
# Set the last marker and submit again.
|
||||
And I set the field "id_drops_3_choice" to "2"
|
||||
And I press "id_submitbutton"
|
||||
And I should see "Drag and drop markers"
|
||||
@@ -0,0 +1,68 @@
|
||||
@qtype @qtype_ddmarker
|
||||
Feature: Test duplicating a quiz containing a drag and drop markers question
|
||||
As a teacher
|
||||
In order re-use my courses containing drag and drop markers questions
|
||||
I need to be able to backup and restore them
|
||||
|
||||
Background:
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddmarker | Drag markers | mkmap |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And quiz "Test quiz" contains the following questions:
|
||||
| Drag markers | 1 |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I follow "Course 1"
|
||||
|
||||
@javascript
|
||||
Scenario: Backup and restore a course containing a drag and drop markers question
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
And I restore "test_backup.mbz" backup into a new course using this options:
|
||||
| Schema | Course name | Course 2 |
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
And I click on "Edit" "link" in the "Drag markers" "table_row"
|
||||
Then the following fields match these values:
|
||||
| Question name | Drag markers |
|
||||
| Question text | Please place the markers on the map of Milton Keynes and be aware that there is more than one railway station. |
|
||||
| General feedback | The Open University is at the junction of Brickhill Street and Groveway. There are three railway stations, Wolverton, Milton Keynes Central and Bletchley. |
|
||||
| Default mark | 1 |
|
||||
| id_shuffleanswers | 0 |
|
||||
| id_drags_0_label | OU |
|
||||
| id_drags_0_noofdrags | 1 |
|
||||
| id_drags_1_label | Railway station |
|
||||
| id_drags_1_noofdrags | 3 |
|
||||
| id_drops_0_shape | Circle |
|
||||
| id_drops_0_coords | 322,213;10 |
|
||||
| id_drops_0_choice | OU |
|
||||
| id_drops_1_shape | Circle |
|
||||
| id_drops_1_coords | 144,84;10 |
|
||||
| id_drops_1_choice | Railway station |
|
||||
| id_drops_2_shape | Circle |
|
||||
| id_drops_2_coords | 195,180;10 |
|
||||
| id_drops_2_choice | Railway station |
|
||||
| id_drops_3_shape | Circle |
|
||||
| id_drops_3_coords | 267,302;10 |
|
||||
| id_drops_3_choice | Railway station |
|
||||
| For any correct response | Well done! |
|
||||
| For any partially correct response | Parts, but only parts, of your response are correct. |
|
||||
| id_shownumcorrect | 1 |
|
||||
| For any incorrect response | That is not right at all. |
|
||||
| Penalty for each incorrect try | 0.3333333 |
|
||||
| Hint 1 | You are trying to place four markers on the map. |
|
||||
| id_hintshownumcorrect_0 | 1 |
|
||||
| id_hintclearwrong_0 | 0 |
|
||||
| id_hintoptions_0 | 0 |
|
||||
| Hint 2 | You are trying to mark three railway stations. |
|
||||
| id_hintshownumcorrect_1 | 1 |
|
||||
| id_hintclearwrong_1 | 1 |
|
||||
| id_hintoptions_1 | 1 |
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
// This file is part of Stack - http://stack.bham.ac.uk/
|
||||
//
|
||||
// Stack 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.
|
||||
//
|
||||
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Behat steps definitions for drag and drop markers.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @category test
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
|
||||
|
||||
/**
|
||||
* Steps definitions related with the drag and drop markers question type.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_qtype_ddmarker extends behat_base {
|
||||
|
||||
/**
|
||||
* Get the xpath for a given drag item.
|
||||
* @param string $dragitem the text of the item to drag.
|
||||
* @return string the xpath expression.
|
||||
*/
|
||||
protected function marker_xpath($marker, $item = 0) {
|
||||
return '//span[contains(@class, " dragitem ") and contains(@class, " item' . $item .
|
||||
'") and span[@class = "markertext" and contains(normalize-space(.), "' .
|
||||
$this->escape($marker) . '")]]';
|
||||
}
|
||||
|
||||
protected function parse_marker_name($marker) {
|
||||
$item = 0;
|
||||
if (preg_match('~,(\d+)$~', $marker, $matches)) {
|
||||
$item = $matches[1];
|
||||
$marker = substr($marker, 0, -1 - strlen($item));
|
||||
}
|
||||
return array($marker, $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag the drag item with the given text to the given space.
|
||||
*
|
||||
* @param string $marker the marker to drag. The label, optionally followed by ,<instance number> (int) if relevant.
|
||||
* @param string $coordinates the position to drag the marker to, 'x,y'.
|
||||
*
|
||||
* @Given /^I drag "(?P<marker>[^"]*)" to "(?P<coordinates>\d+,\d+)" in the drag and drop markers question$/
|
||||
*/
|
||||
public function i_drag_to_in_the_drag_and_drop_markers_question($marker, $coordinates) {
|
||||
list($marker, $item) = $this->parse_marker_name($marker);
|
||||
list($x, $y) = explode(',', $coordinates);
|
||||
|
||||
// This is a bit nasty, but Behat (indeed Selenium) will only drag on
|
||||
// DOM node so that its centre is over the centre of anothe DOM node.
|
||||
// Therefore to make it drag to the specified place, we have to add
|
||||
// a target div.
|
||||
$session = $this->getSession();
|
||||
$session->evaluateScript("
|
||||
(function() {
|
||||
if (document.getElementById('target-{$x}-{$y}')) {
|
||||
return;
|
||||
}
|
||||
var image = document.querySelector('.dropbackground');
|
||||
var target = document.createElement('div');
|
||||
target.setAttribute('id', 'target-{$x}-{$y}');
|
||||
var container = document.querySelector('.droparea');
|
||||
container.style.setProperty('position', 'relative');
|
||||
container.insertBefore(target, image);
|
||||
var xadjusted = {$x} + (container.offsetWidth - image.offsetWidth) / 2
|
||||
target.style.setProperty('position', 'absolute');
|
||||
target.style.setProperty('left', xadjusted + 'px');
|
||||
target.style.setProperty('top', '{$y}px');
|
||||
target.style.setProperty('width', '1px');
|
||||
target.style.setProperty('height', '1px');
|
||||
}())");
|
||||
|
||||
$generalcontext = behat_context_helper::get('behat_general');
|
||||
$generalcontext->i_drag_and_i_drop_it_in($this->marker_xpath($marker, $item),
|
||||
'xpath_element', "#target-{$x}-{$y}", 'css_element');
|
||||
}
|
||||
|
||||
/**
|
||||
* Type some characters while focussed on a given drop box.
|
||||
*
|
||||
* @param string $direction the direction key to press.
|
||||
* @param int $
|
||||
* @param string $marker the marker to drag. The label, optionally followed by ,<instance number> (int) if relevant.
|
||||
*
|
||||
* @Given /^I type "(?P<direction>up|down|left|right)" "(?P<repeats>\d+)" times on marker "(?P<marker>[^"]*)" in the drag and drop markers question$/
|
||||
*/
|
||||
public function i_type_on_marker_in_the_drag_and_drop_markers_question($direction, $repeats, $marker) {
|
||||
$keycodes = array(
|
||||
'up' => chr(38),
|
||||
'down' => chr(40),
|
||||
'left' => chr(37),
|
||||
'right' => chr(39),
|
||||
);
|
||||
list($marker, $item) = $this->parse_marker_name($marker);
|
||||
$node = $this->get_selected_node('xpath_element', $this->marker_xpath($marker, $item));
|
||||
$this->ensure_node_is_visible($node);
|
||||
for ($i = 0; $i < $repeats; $i++) {
|
||||
$node->keyDown($keycodes[$direction]);
|
||||
$node->keyPress($keycodes[$direction]);
|
||||
$node->keyUp($keycodes[$direction]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@qtype @qtype_ddmarker
|
||||
Feature: Test editing a drag and drop markers questions
|
||||
As a teacher
|
||||
In order to be able to update my drag and drop markers questions
|
||||
I need to edit them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddmarker | Drag markers | mkmap |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Edit a drag and drop markers question
|
||||
When I click on "Edit" "link" in the "Drag markers" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Question name | Edited question name |
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Edited question name"
|
||||
@@ -0,0 +1,37 @@
|
||||
@qtype @qtype_ddmarker
|
||||
Feature: Test exporting drag and drop markers questions
|
||||
As a teacher
|
||||
In order to be able to reuse my drag and drop markers questions
|
||||
I need to export them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddmarker | Drag markers | mkmap |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
|
||||
@javascript
|
||||
Scenario: Export a drag and drop markers question
|
||||
# Import sample file.
|
||||
When I navigate to "Export" node in "Course administration > Question bank"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I press "Export questions to file"
|
||||
And following "click here" should download between "297500" and "297700" bytes
|
||||
# If the download step is the last in the scenario then we can sometimes run
|
||||
# into the situation where the download page causes a http redirect but behat
|
||||
# has already conducted its reset (generating an error). By putting a logout
|
||||
# step we avoid behat doing the reset until we are off that page.
|
||||
And I log out
|
||||
@@ -0,0 +1,30 @@
|
||||
@qtype @qtype_ddmarker
|
||||
Feature: Test importing drag and drop markers questions
|
||||
As a teacher
|
||||
In order to reuse drag and drop markers questions
|
||||
I need to import them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import drag and drop markers question.
|
||||
When I navigate to "Import" node in "Course administration > Question bank"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I upload "question/type/ddmarker/tests/fixtures/testquestion.moodle.xml" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Parsing questions from import file."
|
||||
And I should see "Importing 1 questions from file"
|
||||
And I should see "Please place the markers on the map of Milton Keynes and be aware that there is more than one railway station."
|
||||
And I press "Continue"
|
||||
And I should see "Milton Keynes landmarks"
|
||||
@@ -0,0 +1,50 @@
|
||||
@qtype @qtype_ddmarker
|
||||
Feature: Preview a drag-drop onto image question
|
||||
As a teacher
|
||||
In order to check my drag-drop onto image questions will work for students
|
||||
I need to preview them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | ddmarker | Drag markers | mkmap |
|
||||
Given I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a question using the mouse.
|
||||
When I click on "Preview" "link" in the "Drag markers" "table_row"
|
||||
And I switch to "questionpreview" window
|
||||
# Odd, but the <br>s go to nothing, not a space.
|
||||
And I drag "OU" to "340,228" in the drag and drop markers question
|
||||
And I drag "Railway station" to "252,195" in the drag and drop markers question
|
||||
And I drag "Railway station,1" to "324,317" in the drag and drop markers question
|
||||
And I drag "Railway station,2" to "201,99" in the drag and drop markers question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Correct"
|
||||
And I should see "Mark 1.00 out of 1.00"
|
||||
And I switch to the main window
|
||||
|
||||
@javascript
|
||||
Scenario: Preview a question using the keyboard.
|
||||
When I click on "Preview" "link" in the "Drag markers" "table_row"
|
||||
And I switch to "questionpreview" window
|
||||
And I type "up" "89" times on marker "Railway station" in the drag and drop markers question
|
||||
And I type "right" "21" times on marker "Railway station" in the drag and drop markers question
|
||||
And I press "Submit and finish"
|
||||
Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Partially correct"
|
||||
And I should see "Mark 0.25 out of 1.00"
|
||||
And I switch to the main window
|
||||
|
After Width: | Height: | Size: 216 KiB |
@@ -0,0 +1,198 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Test helpers for the drag-and-drop markers question type.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Test helper class for the drag-and-drop markers question type.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_test_helper extends question_test_helper {
|
||||
public function get_test_questions() {
|
||||
return array('fox', 'maths', 'mkmap');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return qtype_ddmarker_question
|
||||
*/
|
||||
public function make_ddmarker_question_fox() {
|
||||
question_bank::load_question_definition_classes('ddmarker');
|
||||
$dd = new qtype_ddmarker_question();
|
||||
|
||||
test_question_maker::initialise_a_question($dd);
|
||||
|
||||
$dd->name = 'Drag-and-drop markers question';
|
||||
$dd->questiontext = 'The quick brown fox jumped over the lazy dog.';
|
||||
$dd->generalfeedback = 'This sentence uses each letter of the alphabet.';
|
||||
$dd->qtype = question_bank::get_qtype('ddmarker');
|
||||
|
||||
$dd->shufflechoices = true;
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_fields($dd);
|
||||
|
||||
$dd->choices = $this->make_choice_structure(array(
|
||||
new qtype_ddmarker_drag_item('quick', 1, 0, 1),
|
||||
new qtype_ddmarker_drag_item('fox', 2, 0, 1),
|
||||
new qtype_ddmarker_drag_item('lazy', 3, 0, 1)
|
||||
|
||||
));
|
||||
|
||||
$dd->places = $this->make_place_structure(array(
|
||||
new qtype_ddmarker_drop_zone(1, 'circle', '50,50;50'),
|
||||
new qtype_ddmarker_drop_zone(2, 'rectangle', '100,0;100,100'),
|
||||
new qtype_ddmarker_drop_zone(3, 'polygon', '0,100;200,100;200,200;0,200')
|
||||
));
|
||||
$dd->rightchoices = array(1 => 1, 2 => 2, 3 => 3);
|
||||
|
||||
return $dd;
|
||||
}
|
||||
|
||||
protected function make_choice_structure($choices) {
|
||||
$choicestructure = array();
|
||||
foreach ($choices as $choice) {
|
||||
$group = $choice->choice_group();
|
||||
if (!isset($choicestructure[$group])) {
|
||||
$choicestructure[$group] = array();
|
||||
}
|
||||
$choicestructure[$group][$choice->no] = $choice;
|
||||
}
|
||||
return $choicestructure;
|
||||
}
|
||||
|
||||
protected function make_place_structure($places) {
|
||||
$placestructure = array();
|
||||
foreach ($places as $place) {
|
||||
$placestructure[$place->no] = $place;
|
||||
}
|
||||
return $placestructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return qtype_ddmarker_question
|
||||
*/
|
||||
public function make_ddmarker_question_maths() {
|
||||
question_bank::load_question_definition_classes('ddmarker');
|
||||
$dd = new qtype_ddmarker_question();
|
||||
|
||||
test_question_maker::initialise_a_question($dd);
|
||||
|
||||
$dd->name = 'Drag-and-drop markers question';
|
||||
$dd->questiontext = 'Fill in the operators to make this equation work: ';
|
||||
$dd->generalfeedback = 'Hmmmm...';
|
||||
$dd->qtype = question_bank::get_qtype('ddmarker');
|
||||
|
||||
$dd->shufflechoices = true;
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_fields($dd);
|
||||
|
||||
$dd->choices = $this->make_choice_structure(array(
|
||||
new qtype_ddmarker_drag_item('+', 1, 1, 0),
|
||||
new qtype_ddmarker_drag_item('-', 2, 1, 0),
|
||||
new qtype_ddmarker_drag_item('*', 3, 1, 0),
|
||||
new qtype_ddmarker_drag_item('/', 4, 1, 0)
|
||||
|
||||
));
|
||||
|
||||
$dd->places = $this->make_place_structure(array(
|
||||
new qtype_ddmarker_drop_zone(1, 'circle', '50,50;50'),
|
||||
new qtype_ddmarker_drop_zone(2, 'rectangle', '100,0;100,100'),
|
||||
new qtype_ddmarker_drop_zone(3, 'polygon', '0,100;100,100;100,200;0,200')
|
||||
));
|
||||
$dd->rightchoices = array(1 => 1, 2 => 1, 3 => 1);
|
||||
|
||||
return $dd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass date to create a ddmarkers question.
|
||||
*/
|
||||
public function get_ddmarker_question_form_data_mkmap() {
|
||||
global $CFG, $USER;
|
||||
$fromform = new stdClass();
|
||||
|
||||
$bgdraftitemid = 0;
|
||||
file_prepare_draft_area($bgdraftitemid, null, null, null, null);
|
||||
$fs = get_file_storage();
|
||||
$filerecord = new stdClass();
|
||||
$filerecord->contextid = context_user::instance($USER->id)->id;
|
||||
$filerecord->component = 'user';
|
||||
$filerecord->filearea = 'draft';
|
||||
$filerecord->itemid = $bgdraftitemid;
|
||||
$filerecord->filepath = '/';
|
||||
$filerecord->filename = 'mkmap.png';
|
||||
$fs->create_file_from_pathname($filerecord, $CFG->dirroot .
|
||||
'/question/type/ddmarker/tests/fixtures/mkmap.png');
|
||||
|
||||
$fromform->name = 'Milton Keynes landmarks';
|
||||
$fromform->questiontext = array(
|
||||
'text' => 'Please place the markers on the map of Milton Keynes and be aware that '.
|
||||
'there is more than one railway station.',
|
||||
'format' => FORMAT_HTML,
|
||||
);
|
||||
$fromform->defaultmark = 1;
|
||||
$fromform->generalfeedback = array(
|
||||
'text' => 'The Open University is at the junction of Brickhill Street and Groveway. '.
|
||||
'There are three railway stations, Wolverton, Milton Keynes Central and Bletchley.',
|
||||
'format' => FORMAT_HTML,
|
||||
);
|
||||
$fromform->bgimage = $bgdraftitemid;
|
||||
$fromform->shuffleanswers = 0;
|
||||
|
||||
$fromform->drags = array(
|
||||
array('label' => 'OU', 'noofdrags' => 1),
|
||||
array('label' => 'Railway station', 'noofdrags' => 3),
|
||||
);
|
||||
|
||||
$fromform->drops = array(
|
||||
array('shape' => 'Circle', 'coords' => '322,213;10', 'choice' => 1),
|
||||
array('shape' => 'Circle', 'coords' => '144,84;10', 'choice' => 2),
|
||||
array('shape' => 'Circle', 'coords' => '195,180;10', 'choice' => 2),
|
||||
array('shape' => 'Circle', 'coords' => '267,302;10', 'choice' => 2),
|
||||
);
|
||||
|
||||
test_question_maker::set_standard_combined_feedback_form_data($fromform);
|
||||
|
||||
$fromform->penalty = '0.3333333';
|
||||
$fromform->hint = array(
|
||||
array(
|
||||
'text' => 'You are trying to place four markers on the map.',
|
||||
'format' => FORMAT_HTML,
|
||||
),
|
||||
array(
|
||||
'text' => 'You are trying to mark three railway stations.',
|
||||
'format' => FORMAT_HTML,
|
||||
),
|
||||
);
|
||||
$fromform->hintshownumcorrect = array(1, 1);
|
||||
$fromform->hintclearwrong = array(0, 1);
|
||||
$fromform->hintoptions = array(0, 1);
|
||||
|
||||
return $fromform;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop markers question definition class.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddmarker/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop markers question definition class.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_question_test extends basic_testcase {
|
||||
|
||||
public function test_get_question_summary() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$this->assertEquals('The quick brown fox jumped over the lazy dog.; '.
|
||||
'[[Drop zone 1]] -> {quick / fox / lazy}; '.
|
||||
'[[Drop zone 2]] -> {quick / fox / lazy}; '.
|
||||
'[[Drop zone 3]] -> {quick / fox / lazy}',
|
||||
$dd->get_question_summary());
|
||||
}
|
||||
|
||||
public function test_get_question_summary_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$this->assertEquals('Fill in the operators to make this equation work:; '.
|
||||
'[[Drop zone 1]] -> {+ / - / * / /}; '.
|
||||
'[[Drop zone 2]] -> {+ / - / * / /}; '.
|
||||
'[[Drop zone 3]] -> {+ / - / * / /}',
|
||||
$dd->get_question_summary());
|
||||
}
|
||||
|
||||
public function test_summarise_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals('{Drop zone 1 -> quick}, '.
|
||||
'{Drop zone 2 -> fox}, '.
|
||||
'{Drop zone 3 -> lazy}',
|
||||
$dd->summarise_response(array('c1' => '50,50',
|
||||
'c2' => '150,50',
|
||||
'c3' => '50,150')));
|
||||
}
|
||||
|
||||
public function test_summarise_response_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals('{Drop zone 1 -> +}, '.
|
||||
'{Drop zone 2 -> +}, '.
|
||||
'{Drop zone 3 -> +}',
|
||||
$dd->summarise_response(array('c1' => '50,50;150,50;50,150',
|
||||
'c2' => '',
|
||||
'c3' => '')));
|
||||
}
|
||||
|
||||
public function test_get_random_guess_score() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$this->assertEquals(null, $dd->get_random_guess_score());
|
||||
}
|
||||
|
||||
public function test_get_random_guess_score_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$this->assertEquals(null, $dd->get_random_guess_score());
|
||||
}
|
||||
|
||||
public function test_get_right_choice_for() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(1));
|
||||
$this->assertEquals(2, $dd->get_right_choice_for(2));
|
||||
$this->assertEquals(3, $dd->get_right_choice_for(3));
|
||||
}
|
||||
|
||||
public function test_get_right_choice_for_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(1));
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(2));
|
||||
$this->assertEquals(1, $dd->get_right_choice_for(3));
|
||||
}
|
||||
|
||||
public function test_clear_wrong_from_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$initialresponse = array('c1' => '50,50', 'c2' => '100,100', 'c3' => '100,100;200,200');
|
||||
$this->assertEquals(array('c1' => '50,50', 'c2' => '', 'c3' => ''),
|
||||
$dd->clear_wrong_from_response($initialresponse));
|
||||
}
|
||||
|
||||
public function test_get_num_parts_right() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
// The second returned param in array is the max of correct choices or
|
||||
// the actual number of items dragged.
|
||||
$response1 = array('c1' => '50,50', 'c2' => '100,100', 'c3' => '100,100;200,200');
|
||||
$this->assertEquals(array(1, 4), $dd->get_num_parts_right($response1));
|
||||
$response2 = array('c1' => '50,50;150,50;50,150',
|
||||
'c2' => '100,100',
|
||||
'c3' => '100,100;200,200');
|
||||
$this->assertEquals(array(1, 6), $dd->get_num_parts_right($response2));
|
||||
$response3 = array('c1' => '50,50;150,50;50,150',
|
||||
'c2' => '',
|
||||
'c3' => '');
|
||||
$this->assertEquals(array(1, 3), $dd->get_num_parts_right($response3));
|
||||
}
|
||||
|
||||
public function test_get_num_parts_right_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(3, 3),
|
||||
$dd->get_num_parts_right(array(
|
||||
'c1' => '50,50;150,50;50,150', 'c2' => '', 'c3' => '')));
|
||||
}
|
||||
|
||||
public function test_get_expected_data() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(
|
||||
array('c1' => PARAM_NOTAGS, 'c2' => PARAM_NOTAGS, 'c3' => PARAM_NOTAGS),
|
||||
$dd->get_expected_data()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_get_correct_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150'),
|
||||
$dd->get_correct_response());
|
||||
}
|
||||
|
||||
public function test_get_correct_response_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array('c1' => '50,50;150,50;50,150'), $dd->get_correct_response());
|
||||
}
|
||||
|
||||
public function test_is_same_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertTrue($dd->is_same_response(
|
||||
array(),
|
||||
array('c1' => '', 'c2' => '', 'c3' => '', 'c4' => '')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array(),
|
||||
array('c1' => '100,100', 'c2' => '', 'c3' => '', 'c4' => '')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array('c1' => '', 'c2' => '', 'c3' => '', 'c4' => ''),
|
||||
array('c1' => '100,100', 'c2' => '', 'c3' => '', 'c4' => '')));
|
||||
|
||||
$this->assertTrue($dd->is_same_response(
|
||||
array('c1' => '100,100', 'c2' => '2', 'c3' => '3', 'c4' => '400,400'),
|
||||
array('c1' => '100,100', 'c2' => '2', 'c3' => '3', 'c4' => '400,400')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array('c1' => '100,100', 'c2' => '200,200', 'c3' => '300,300', 'c4' => '400,400'),
|
||||
array('c1' => '100,100', 'c2' => '200,200', 'c3' => '200,200', 'c4' => '400,400')));
|
||||
|
||||
$this->assertTrue($dd->is_same_response(
|
||||
array('c1' => '100,100;200,200', 'c2' => '',
|
||||
'c3' => '100,100;300,300', 'c4' => '400,400'),
|
||||
array('c1' => '200,200;100,100', 'c2' => '',
|
||||
'c3' => '300,300;100,100', 'c4' => '400,400')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array('c1' => '100,100;200,200', 'c2' => '',
|
||||
'c3' => '100,100;400,300', 'c4' => '400,400'),
|
||||
array('c1' => '200,200;100,100', 'c2' => '',
|
||||
'c3' => '300,300;100,100', 'c4' => '400,400')));
|
||||
|
||||
$this->assertTrue($dd->is_same_response(
|
||||
array('c1' => '100,100;100,100;200,200', 'c2' => '',
|
||||
'c3' => '100,100;300,300', 'c4' => '400,400'),
|
||||
array('c1' => '200,200;100,100;100,100', 'c2' => '',
|
||||
'c3' => '300,300;100,100', 'c4' => '400,400')));
|
||||
|
||||
$this->assertFalse($dd->is_same_response(
|
||||
array('c1' => '100,100;100,100;200,200', 'c2' => '',
|
||||
'c3' => '100,100;300,300', 'c4' => '400,400'),
|
||||
array('c1' => '200,200;100,100', 'c2' => '',
|
||||
'c3' => '300,300;100,100', 'c4' => '400,400')));
|
||||
}
|
||||
public function test_is_complete_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertFalse($dd->is_complete_response(array()));
|
||||
$this->assertFalse($dd->is_complete_response(
|
||||
array('c1' => '', 'c2' => '', 'c3' => '')));
|
||||
$this->assertFalse($dd->is_complete_response(array('c1' => '')));
|
||||
$this->assertTrue($dd->is_complete_response(
|
||||
array('c1' => '300,300', 'c2' => '300,300', 'c3' => '300,300')));
|
||||
}
|
||||
|
||||
public function test_is_gradable_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertFalse($dd->is_gradable_response(array()));
|
||||
$this->assertFalse($dd->is_gradable_response(
|
||||
array('c1' => '', 'c2' => '', 'c3' => '', 'c3' => '')));
|
||||
$this->assertTrue($dd->is_gradable_response(
|
||||
array('c1' => '300,300', 'c2' => '300,300', 'c3' => '')));
|
||||
$this->assertTrue($dd->is_gradable_response(array('c1' => '300,300')));
|
||||
$this->assertTrue($dd->is_gradable_response(
|
||||
array('c1' => '300,300', 'c2' => '300,300', 'c3' => '300,300')));
|
||||
}
|
||||
|
||||
public function test_grading() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(1, question_state::$gradedright),
|
||||
$dd->grade_response(array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150')));
|
||||
$this->assertEquals(array(2 / 3, question_state::$gradedpartial),
|
||||
$dd->grade_response(array('c1' => '50,50', 'c2' => '50,50', 'c3' => '100,150')));
|
||||
$this->assertEquals(array(0, question_state::$gradedwrong),
|
||||
$dd->grade_response(array('c1' => '150,50', 'c2' => '50,50', 'c3' => '100,50')));
|
||||
}
|
||||
|
||||
public function test_grading_maths() {
|
||||
$dd = test_question_maker::make_question('ddmarker', 'maths');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(1, question_state::$gradedright),
|
||||
$dd->grade_response(array('c1' => '50,50;150,50;50,150', 'c2' => '', 'c3' => '')));
|
||||
$this->assertEquals(array(0.75, question_state::$gradedpartial),
|
||||
$dd->grade_response(array('c1' => '50,50;150,50;50,150',
|
||||
'c2' => '', 'c3' => '50,150')));
|
||||
$this->assertEquals(array(0, question_state::$gradedwrong),
|
||||
$dd->grade_response(array('c1' => '', 'c2' => '50,50;150,50', 'c3' => '100,50')));
|
||||
$this->assertEquals(array(0, question_state::$gradedwrong),
|
||||
$dd->grade_response(array('c1' => '300,300',
|
||||
'c2' => '50,50;150,50',
|
||||
'c3' => '100,50')));
|
||||
}
|
||||
|
||||
public function test_classify_response() {
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$dd->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$this->assertEquals(array(
|
||||
1 => new question_classified_response(1, 'quick', 1 / 3),
|
||||
2 => new question_classified_response(2, 'fox', 1 / 3),
|
||||
3 => new question_classified_response(3, 'lazy', 1 / 3)),
|
||||
$dd->classify_response(array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150')));
|
||||
|
||||
$this->assertEquals(array(
|
||||
1 => new question_classified_response(1, 'quick', 1 / 3),
|
||||
2 => question_classified_response::no_response(),
|
||||
3 => question_classified_response::no_response()),
|
||||
$dd->classify_response(array('c1' => '50,50', 'c2' => '100,150', 'c3' => '150,50')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop markers question definition class.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddmarker/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop markers question definition class.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_test extends basic_testcase {
|
||||
/** @var qtype_ddmarker instance of the question type class to test. */
|
||||
protected $qtype;
|
||||
|
||||
protected function setUp() {
|
||||
$this->qtype = question_bank::get_qtype('ddmarker');;
|
||||
}
|
||||
|
||||
protected function tearDown() {
|
||||
$this->qtype = null;
|
||||
}
|
||||
|
||||
public function test_name() {
|
||||
$this->assertEquals($this->qtype->name(), 'ddmarker');
|
||||
}
|
||||
|
||||
public function test_can_analyse_responses() {
|
||||
$this->assertTrue($this->qtype->can_analyse_responses());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop words shape code.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/ddmarker/shapes.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for shape code
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_shapes_test extends basic_testcase {
|
||||
|
||||
public function test_polygon_valdiation_test_ok() {
|
||||
$shape = new qtype_ddmarker_shape_polygon('10, 10; 20, 10; 20, 20; 10, 20');
|
||||
$this->assertFalse($shape->get_coords_interpreter_error()); // No errors.
|
||||
}
|
||||
|
||||
public function test_polygon_valdiation_test_only_two_points() {
|
||||
$shape = new qtype_ddmarker_shape_polygon('10, 10; 20, 10');
|
||||
$this->assertEquals(get_string('formerror_polygonmusthaveatleastthreepoints', 'qtype_ddmarker',
|
||||
array('shape' => 'polygon', 'coordsstring' => get_string('shape_polygon_coords', 'qtype_ddmarker'))),
|
||||
$shape->get_coords_interpreter_error());
|
||||
}
|
||||
|
||||
public function test_polygon_valdiation_test_invalid_point() {
|
||||
$shape = new qtype_ddmarker_shape_polygon('10, 10; 20, ; 20, 20; 10, 20');
|
||||
$this->assertEquals(get_string('formerror_onlyusewholepositivenumbers', 'qtype_ddmarker',
|
||||
array('shape' => 'polygon', 'coordsstring' => get_string('shape_polygon_coords', 'qtype_ddmarker'))),
|
||||
$shape->get_coords_interpreter_error());
|
||||
}
|
||||
|
||||
public function test_polygon_valdiation_test_repeated_point() {
|
||||
$shape = new qtype_ddmarker_shape_polygon('70,220;90,200;95,150;120,150;140,200;150,230;'.
|
||||
'150,230;150,240;120,240;110,240;90,240');
|
||||
$this->assertEquals(get_string('formerror_repeatedpoint', 'qtype_ddmarker',
|
||||
array('shape' => 'polygon', 'coordsstring' => get_string('shape_polygon_coords', 'qtype_ddmarker'))),
|
||||
$shape->get_coords_interpreter_error());
|
||||
}
|
||||
|
||||
public function test_polygon_hit_test() {
|
||||
$shape = new qtype_ddmarker_shape_polygon('10, 10; 20, 10; 20, 20; 10, 20');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(15, 15)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(5, 5)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(5, 15)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(15, 25)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(25, 15)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(11, 11)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(19, 19)));
|
||||
|
||||
// Should accept closed polygon coords or unclosed and it will model a closed polygon.
|
||||
$shape = new qtype_ddmarker_shape_polygon('10, 10; 20, 10; 20, 20; 10, 20; 10, 10');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(15, 15)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(5, 5)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(5, 15)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(15, 25)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(25, 15)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(11, 11)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(19, 19)));
|
||||
|
||||
$shape = new qtype_ddmarker_shape_polygon('10, 10; 15, 5; 20, 10; 20, 20; 10, 20');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(15, 15)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(5, 5)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(5, 15)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(15, 25)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(25, 15)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(11, 11)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(19, 19)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(15, 9)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(15, 10)));
|
||||
|
||||
$shape = new qtype_ddmarker_shape_polygon('15, 5; 20, 10; 20, 20; 10, 20; 10, 10');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(15, 10)));
|
||||
|
||||
$shape = new qtype_ddmarker_shape_polygon('15, 5; 20, 10; 20, 20; 10, 20; 10, 10');
|
||||
$this->assertFalse($shape->is_point_in_shape(array(25, 10)));
|
||||
|
||||
$shape = new qtype_ddmarker_shape_polygon('0, 0; 500, 0; 600, 1000; 0, 1200; 10, 10');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(25, 10)));
|
||||
}
|
||||
|
||||
public function test_circle_valdiation_test() {
|
||||
$shape = new qtype_ddmarker_shape_circle('10, 10; 10');
|
||||
$this->assertFalse($shape->get_coords_interpreter_error()); // No errors.
|
||||
}
|
||||
|
||||
public function test_circle_hit_test() {
|
||||
$shape = new qtype_ddmarker_shape_circle('10, 10; 10');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(19, 10)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(20, 10)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(10, 1)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(15, 25)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(25, 15)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(11, 11)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(1, 10)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(17, 17)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(3, 3)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(2, 2)));
|
||||
}
|
||||
|
||||
public function test_rectangle_valdiation_test() {
|
||||
$shape = new qtype_ddmarker_shape_rectangle('1000, 4000; 500, 400');
|
||||
$this->assertFalse($shape->get_coords_interpreter_error()); // No errors.
|
||||
}
|
||||
|
||||
public function test_rectangle_hit_test() {
|
||||
$shape = new qtype_ddmarker_shape_rectangle('1000, 4000; 500, 400');
|
||||
$this->assertTrue($shape->is_point_in_shape(array(1001, 4001)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(1000, 4000)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(501, 3601)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(1499, 4399)));
|
||||
$this->assertFalse($shape->is_point_in_shape(array(25, 15)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(1001, 4399)));
|
||||
$this->assertTrue($shape->is_point_in_shape(array(1499, 4001)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,722 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop markers question type.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/ddmarker/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the drag-and-drop markers question type.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_walkthrough_test extends qbehaviour_walkthrough_test_base {
|
||||
|
||||
/**
|
||||
* Get an expectation that the output contains a marker.
|
||||
* @param unknown $choice which choice.
|
||||
* @param unknown $infinite whether there are infinitely many of that choice.
|
||||
* @return question_contains_tag_with_attributes the expectation.
|
||||
*/
|
||||
protected function get_contains_draggable_marker_home_expectation($choice, $infinite) {
|
||||
$class = 'draghome choice'.$choice;
|
||||
if ($infinite) {
|
||||
$class .= ' infinite';
|
||||
}
|
||||
|
||||
$expectedattrs = array();
|
||||
$expectedattrs['class'] = $class;
|
||||
|
||||
return new question_contains_tag_with_attributes('span', $expectedattrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPdoc)
|
||||
* @see qbehaviour_walkthrough_test_base::get_contains_hidden_expectation()
|
||||
*/
|
||||
protected function get_contains_hidden_expectation($choiceno, $value = null) {
|
||||
$name = $this->quba->get_field_prefix($this->slot) .'c'. $choiceno;
|
||||
$expectedattributes = array('type' => 'hidden', 'name' => s($name));
|
||||
$expectedattributes['class'] = "choices choice{$choiceno}";
|
||||
if (!is_null($value)) {
|
||||
$expectedattributes['value'] = s($value);
|
||||
}
|
||||
return new question_contains_tag_with_attributes('input', $expectedattributes);
|
||||
}
|
||||
|
||||
public function test_interactive_behaviour() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->hints = array(
|
||||
new question_hint_ddmarker(13, 'This is the first hint.',
|
||||
FORMAT_HTML, false, false, false),
|
||||
new question_hint_ddmarker(14, 'This is the second hint.',
|
||||
FORMAT_HTML, true, true, false),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
$completelywrong = array('c1' => '0,250', 'c2' => '100,250', 'c3' => '150,250');
|
||||
// Save the wrong answer.
|
||||
$this->process_submission($completelywrong);
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '0,250'),
|
||||
$this->get_contains_hidden_expectation(2, '100,250'),
|
||||
$this->get_contains_hidden_expectation(3, '150,250'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
// Submit the wrong answer.
|
||||
$this->process_submission($completelywrong + array('-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '0,250'),
|
||||
$this->get_contains_hidden_expectation(2, '100,250'),
|
||||
$this->get_contains_hidden_expectation(3, '150,250'),
|
||||
$this->get_contains_try_again_button_expectation(true),
|
||||
$this->get_contains_hint_expectation('This is the first hint'));
|
||||
|
||||
// Do try again.
|
||||
$this->process_submission(array('-tryagain' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '0,250'),
|
||||
$this->get_contains_hidden_expectation(2, '100,250'),
|
||||
$this->get_contains_hidden_expectation(3, '150,250'),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(2),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit the right answer.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(8);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, '100,150'),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_correct_expectation(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(8);
|
||||
}
|
||||
|
||||
public function test_deferred_feedback() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Save a partial answer.
|
||||
$this->process_submission(array('c1' => '150,50', 'c2' => '50,50'));
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '150,50'),
|
||||
$this->get_contains_hidden_expectation(2, '50,50'),
|
||||
$this->get_contains_hidden_expectation(3, ''),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
// Save the right answer.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150'));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, '100,150'),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(12);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, '100,150'),
|
||||
$this->get_contains_correct_expectation());
|
||||
|
||||
// Change the right answer a bit.
|
||||
$dd->rightchoices[2] = 1;
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(8);
|
||||
}
|
||||
|
||||
public function test_deferred_feedback_unanswered() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
$this->check_step_count(1);
|
||||
|
||||
// Save a blank response.
|
||||
$this->process_submission(array('c1' => '', 'c2' => '', 'c3' => ''));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, ''),
|
||||
$this->get_contains_hidden_expectation(2, ''),
|
||||
$this->get_contains_hidden_expectation(3, ''),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
$this->check_step_count(1);
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gaveup);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false));
|
||||
}
|
||||
|
||||
public function test_deferred_feedback_partial_answer() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
$this->process_submission(array('c1' => '50,50', 'c2' => '150,50', 'c3' => ''));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, ''),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(2);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_partcorrect_expectation());
|
||||
}
|
||||
|
||||
public function test_interactive_grading() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->hints = array(
|
||||
new question_hint_ddmarker(1, 'This is the first hint.',
|
||||
FORMAT_MOODLE, true, true, false),
|
||||
new question_hint_ddmarker(2, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true, false),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 12);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->assertEquals('interactivecountback',
|
||||
$this->quba->get_question_attempt($this->slot)->get_behaviour_name());
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_does_not_contain_num_parts_correct(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit an response with the first two parts right.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '150,50', 'c3' => '150,50', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_try_again_button_expectation(true),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_contains_hint_expectation('This is the first hint'),
|
||||
$this->get_contains_num_parts_correct(2),
|
||||
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, '150,50'));
|
||||
|
||||
// Check that extract responses will return the reset data.
|
||||
$prefix = $this->quba->get_field_prefix($this->slot);
|
||||
$this->assertEquals(array('c1' => '50,50', 'c2' => '150,50'),
|
||||
$this->quba->extract_responses($this->slot,
|
||||
array($prefix . 'c1' => '50,50', $prefix . 'c2' => '150,50', '-tryagain' => 1)));
|
||||
|
||||
// Do try again.
|
||||
// keys c3 is an extra hidden fields to clear data.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '150,50', 'c3' => '', '-tryagain' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, ''),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_try_again_button_expectation(),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(2),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit an response with the first and last parts right.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '150,150', 'c3' => '100,150', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_try_again_button_expectation(true),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_contains_hint_expectation('This is the second hint'),
|
||||
$this->get_contains_num_parts_correct(2),
|
||||
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,150'),
|
||||
$this->get_contains_hidden_expectation(3, '100,150'));
|
||||
|
||||
// Do try again.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '', 'c3' => '', '-tryagain' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, ''),
|
||||
$this->get_contains_hidden_expectation(3, ''),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_try_again_button_expectation(),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(1),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Submit the right answer.
|
||||
$this->process_submission(
|
||||
array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(8);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, '50,50'),
|
||||
$this->get_contains_hidden_expectation(2, '150,50'),
|
||||
$this->get_contains_hidden_expectation(3, '100,150'),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_does_not_contain_try_again_button_expectation(),
|
||||
$this->get_contains_correct_expectation(),
|
||||
$this->get_no_hint_visible_expectation(),
|
||||
$this->get_does_not_contain_num_parts_correct(),
|
||||
$this->get_contains_standard_correct_combined_feedback_expectation());
|
||||
}
|
||||
|
||||
public function test_interactive_correct_no_submit() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->hints = array(
|
||||
new question_hint_ddmarker(23, 'This is the first hint.',
|
||||
FORMAT_MOODLE, false, false, false),
|
||||
new question_hint_ddmarker(24, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true, false),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the right answer.
|
||||
$this->process_submission(array('c1' => '50,50', 'c2' => '150,50', 'c3' => '100,150'));
|
||||
|
||||
// Finish the attempt without clicking check.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_correct_expectation(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
}
|
||||
|
||||
public function test_interactive_partial_no_submit() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->hints = array(
|
||||
new question_hint_ddmarker(23, 'This is the first hint.',
|
||||
FORMAT_MOODLE, false, false, false),
|
||||
new question_hint_ddmarker(24, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true, false),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the a partially right answer.
|
||||
$this->process_submission(array('c1' => '50,50', 'c2' => '50,50', 'c3' => '100,150'));
|
||||
|
||||
// Finish the attempt without clicking check.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(2);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_partcorrect_expectation(),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Check regrading does not mess anything up.
|
||||
$this->quba->regrade_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedpartial);
|
||||
$this->check_current_mark(2);
|
||||
}
|
||||
|
||||
public function test_interactive_no_right_clears() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$dd->hints = array(
|
||||
new question_hint_ddmarker(23, 'This is the first hint.',
|
||||
FORMAT_MOODLE, false, true, false),
|
||||
new question_hint_ddmarker(24, 'This is the second hint.',
|
||||
FORMAT_MOODLE, true, true, false),
|
||||
);
|
||||
$dd->shufflechoices = false;
|
||||
$this->start_attempt_at_question($dd, 'interactive', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_marked_out_of_summary(),
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(3),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
|
||||
// Save the a completely wrong answer.
|
||||
$this->process_submission(
|
||||
array('c1' => '100,150', 'c2' => '100,150', 'c3' => '50,50', '-submit' => 1));
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_marked_out_of_summary(),
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_submit_button_expectation(false),
|
||||
$this->get_contains_hint_expectation('This is the first hint'));
|
||||
|
||||
// Do try again.
|
||||
$this->process_submission(
|
||||
array('c1' => '', 'c2' => '', 'c3' => '', '-tryagain' => 1));
|
||||
|
||||
// Check that all the wrong answers have been cleared.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_marked_out_of_summary(),
|
||||
$this->get_contains_draggable_marker_home_expectation(1, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(2, false),
|
||||
$this->get_contains_draggable_marker_home_expectation(3, false),
|
||||
$this->get_contains_hidden_expectation(1, ''),
|
||||
$this->get_contains_hidden_expectation(2, ''),
|
||||
$this->get_contains_hidden_expectation(3, ''),
|
||||
$this->get_contains_submit_button_expectation(true),
|
||||
$this->get_does_not_contain_feedback_expectation(),
|
||||
$this->get_tries_remaining_expectation(2),
|
||||
$this->get_no_hint_visible_expectation());
|
||||
}
|
||||
|
||||
public function test_display_of_right_answer_when_shuffled() {
|
||||
|
||||
// Create a drag-and-drop question.
|
||||
$dd = test_question_maker::make_question('ddmarker');
|
||||
$this->start_attempt_at_question($dd, 'deferredfeedback', 3);
|
||||
|
||||
// Check the initial state.
|
||||
$this->check_current_state(question_state::$todo);
|
||||
$this->check_current_mark(null);
|
||||
|
||||
$this->check_current_output(
|
||||
$this->get_contains_hidden_expectation(1),
|
||||
$this->get_contains_hidden_expectation(2),
|
||||
$this->get_contains_hidden_expectation(3),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Save a partial answer.
|
||||
$this->process_submission($dd->get_correct_response());
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
$rightanswer = array($dd->get_right_choice_for(1) => '50,50',
|
||||
$dd->get_right_choice_for(2) => '150,50',
|
||||
$dd->get_right_choice_for(3) => '100,150');
|
||||
$this->check_current_output(
|
||||
$this->get_contains_hidden_expectation(1, $rightanswer[1]),
|
||||
$this->get_contains_hidden_expectation(2, $rightanswer[2]),
|
||||
$this->get_contains_hidden_expectation(3, $rightanswer[3]),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation());
|
||||
|
||||
// Finish the attempt.
|
||||
$this->quba->finish_all_questions();
|
||||
|
||||
// Verify.
|
||||
$this->displayoptions->rightanswer = question_display_options::VISIBLE;
|
||||
$this->assertEquals('{Drop zone 1 -> quick}, '.
|
||||
'{Drop zone 2 -> fox}, '.
|
||||
'{Drop zone 3 -> lazy}',
|
||||
$dd->get_right_answer_summary());
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Version information for the drag-and-drop markers question type.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2015091100;
|
||||
$plugin->requires = 2015050500;
|
||||
|
||||
$plugin->component = 'qtype_ddmarker';
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
$plugin->dependencies = array(
|
||||
'qtype_gapselect' => 2015091100,
|
||||
'qtype_ddimageortext' => 2015091100,
|
||||
);
|
||||
@@ -0,0 +1,607 @@
|
||||
YUI.add('moodle-qtype_ddmarker-dd', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
var DDMARKERDDNAME = 'moodle-qtype_ddmarker-dd';
|
||||
var DDMARKER_DD = function() {
|
||||
DDMARKER_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDMARKER_DD, Y.Base, {
|
||||
doc : null,
|
||||
polltimer : null,
|
||||
afterimageloaddone : false,
|
||||
graphics : null,
|
||||
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
if (bgdone) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure : function () {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node : function() {
|
||||
return topnode;
|
||||
},
|
||||
bg_img : function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img : function (url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
drag_items : function() {
|
||||
return dragitemsarea.all('.dragitem');
|
||||
},
|
||||
drag_items_for_choice : function(choiceno) {
|
||||
return dragitemsarea.all('span.dragitem.choice' + choiceno);
|
||||
},
|
||||
drag_item_for_choice : function(choiceno, itemno) {
|
||||
return dragitemsarea.one('span.dragitem.choice' + choiceno +
|
||||
'.item' + itemno);
|
||||
},
|
||||
drag_item_being_dragged : function(choiceno) {
|
||||
return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
|
||||
},
|
||||
drag_item_home : function (choiceno) {
|
||||
return dragitemsarea.one('span.draghome.choice' + choiceno);
|
||||
},
|
||||
drag_item_homes : function() {
|
||||
return dragitemsarea.all('span.draghome');
|
||||
},
|
||||
get_classname_numeric_suffix : function(node, prefix) {
|
||||
var classes = node.getAttribute('class');
|
||||
if (classes !== '') {
|
||||
var classesarr = classes.split(' ');
|
||||
for (var index = 0; index < classesarr.length; index++) {
|
||||
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
|
||||
if (patt1.test(classesarr[index])) {
|
||||
var patt2 = new RegExp('([0-9])+$');
|
||||
var match = patt2.exec(classesarr[index]);
|
||||
return Number(match[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputs_for_choices : function () {
|
||||
return topnode.all('input.choices');
|
||||
},
|
||||
input_for_choice : function (choiceno) {
|
||||
return topnode.one('input.choice' + choiceno);
|
||||
},
|
||||
marker_texts : function () {
|
||||
return topnode.one('div.markertexts');
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
colours : ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
|
||||
'#87CEFA','#DAA520', '#FFD700', '#F0E68C'],
|
||||
nextcolourindex : 0,
|
||||
restart_colours : function () {
|
||||
this.nextcolourindex = 0;
|
||||
},
|
||||
get_next_colour : function () {
|
||||
var colour = this.colours[this.nextcolourindex];
|
||||
this.nextcolourindex++;
|
||||
if (this.nextcolourindex === this.colours.length) {
|
||||
this.nextcolourindex = 0;
|
||||
}
|
||||
return colour;
|
||||
},
|
||||
convert_to_window_xy : function (bgimgxy) {
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
},
|
||||
shapes : [],
|
||||
draw_drop_zone : function (dropzoneno, markertext, shape, coords, colour, link) {
|
||||
var existingmarkertext;
|
||||
if (link) {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
|
||||
} else {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
|
||||
}
|
||||
|
||||
if (existingmarkertext) {
|
||||
if (markertext !== '') {
|
||||
existingmarkertext.setContent(markertext);
|
||||
} else {
|
||||
existingmarkertext.remove(true);
|
||||
}
|
||||
} else if (markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropzoneno;
|
||||
if (link) {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
|
||||
markertext + '</a></span>');
|
||||
} else {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '">' +
|
||||
markertext + '</span>');
|
||||
}
|
||||
}
|
||||
var drawfunc = 'draw_shape_' + shape;
|
||||
if (this[drawfunc] instanceof Function){
|
||||
var xyfortext = this[drawfunc](dropzoneno, coords, colour);
|
||||
if (xyfortext !== null) {
|
||||
var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
|
||||
if (markerspan !== null) {
|
||||
markerspan.setStyle('opacity', '0.6');
|
||||
xyfortext[0] -= markerspan.get('offsetWidth') / 2;
|
||||
xyfortext[1] -= markerspan.get('offsetHeight') / 2;
|
||||
markerspan.setXY(this.convert_to_window_xy(xyfortext));
|
||||
var markerspananchor = markerspan.one('a');
|
||||
if (markerspananchor !== null) {
|
||||
markerspananchor.once('click', function (e, dropzoneno) {
|
||||
var fill = this.shapes[dropzoneno].get('fill');
|
||||
fill.opacity = 1;
|
||||
this.shapes[dropzoneno].set('fill', fill);
|
||||
},
|
||||
this,
|
||||
dropzoneno
|
||||
);
|
||||
markerspananchor.set('tabIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
draw_shape_circle : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 4) {
|
||||
var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
|
||||
if (this.coords_in_img(xy)) {
|
||||
var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'circle',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
draw_shape_rectangle : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 5) {
|
||||
var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
|
||||
if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'rect',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
},
|
||||
draw_shape_polygon : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.split(';');
|
||||
var xy = [];
|
||||
for (var i in coordsparts) {
|
||||
var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
|
||||
if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
|
||||
xy[xy.length] = [parts[1], parts[2]];
|
||||
}
|
||||
}
|
||||
if (xy.length > 2) {
|
||||
var polygon = this.graphics.addShape({
|
||||
type: "path",
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
},
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity : "0.5"
|
||||
}
|
||||
});
|
||||
var maxxy = [0,0];
|
||||
var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
|
||||
for (i = 0; i < xy.length; i++) {
|
||||
//calculate min and max points to find center to show marker on
|
||||
minxy[0] = Math.min(xy[i][0], minxy[0]);
|
||||
minxy[1] = Math.min(xy[i][1], minxy[1]);
|
||||
maxxy[0] = Math.max(xy[i][0], maxxy[0]);
|
||||
maxxy[1] = Math.max(xy[i][1], maxxy[1]);
|
||||
if (i === 0) {
|
||||
polygon.moveTo(xy[i][0], xy[i][1]);
|
||||
} else {
|
||||
polygon.lineTo(xy[i][0], xy[i][1]);
|
||||
}
|
||||
}
|
||||
if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
|
||||
polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
|
||||
}
|
||||
polygon.end();
|
||||
polygon.setXY(this.doc.bg_img().getXY());
|
||||
this.shapes[dropzoneno] = polygon;
|
||||
return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
coords_in_img : function (coords) {
|
||||
return (coords[0] <= this.doc.bg_img().get('width') &&
|
||||
coords[1] <= this.doc.bg_img().get('height'));
|
||||
}
|
||||
}, {
|
||||
NAME : DDMARKERDDNAME,
|
||||
ATTRS : {
|
||||
drops : {value : null},
|
||||
readonly : {value : false},
|
||||
topnode : {value : null}
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
|
||||
|
||||
var DDMARKERQUESTIONNAME = 'ddmarker_question';
|
||||
var DDMARKER_QUESTION = function() {
|
||||
DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
|
||||
touchscrolldisable: null,
|
||||
pendingid: '',
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddmarker-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.after_image_load);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.after_image_load);
|
||||
},
|
||||
after_image_load : function () {
|
||||
this.redraw_drags_and_drops();
|
||||
M.util.js_complete(this.pendingid);
|
||||
Y.later(2000, this, this.redraw_drags_and_drops, [], true);
|
||||
},
|
||||
clone_new_drag_item : function (draghome, itemno) {
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('dragitem');
|
||||
drag.addClass('item' + itemno);
|
||||
drag.one('span.markertext').setStyle('opacity', 0.6);
|
||||
draghome.insert(drag, 'after');
|
||||
if (!this.get('readonly')) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
return drag;
|
||||
},
|
||||
|
||||
/**
|
||||
* prevent_touchmove_from_scrolling allows users of touch screen devices to
|
||||
* use drag and drop and normal scrolling at the same time. I.e.when
|
||||
* touching and dragging a draggable item, the screen does not scroll, but
|
||||
* you can scroll by touching other area of the screen apart from the
|
||||
* draggable items.
|
||||
*/
|
||||
prevent_touchmove_from_scrolling : function(drag) {
|
||||
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
|
||||
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
|
||||
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
|
||||
|
||||
// Disable scrolling when touching the draggable items.
|
||||
drag.on(touchstart, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
return; // Already disabled.
|
||||
}
|
||||
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
});
|
||||
}, this);
|
||||
|
||||
// Allow scrolling after releasing the draggable items.
|
||||
drag.on(touchend, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
this.touchscrolldisable.detach();
|
||||
this.touchscrolldisable = null;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
draggable : function (drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'intersect'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
|
||||
dd.after('drag:start', function(e){
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.addClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
var itemno = this.get_itemno_for_node(dragnode);
|
||||
if (itemno !== null) {
|
||||
dragnode.removeClass('item' + dragnode);
|
||||
}
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
dd.after('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.removeClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
this.save_all_xy_for_choice(choiceno, dragnode);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
//--- keyboard accessibility
|
||||
drag.set('tabIndex', 0);
|
||||
drag.on('dragchange', this.drop_zone_key_press, this);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(drag);
|
||||
},
|
||||
|
||||
save_all_xy_for_choice: function (choiceno, dropped) {
|
||||
var coords = [];
|
||||
var bgimgxy;
|
||||
for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (dragitem) {
|
||||
dragitem.removeClass('item' + i);
|
||||
if (!dragitem.hasClass('beingdragged')) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
dragitem.removeClass('item' + i);
|
||||
dragitem.addClass('item' + coords.length);
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped !== null){
|
||||
bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
|
||||
dropped.addClass('item' + coords.length);
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
this.set_form_value(choiceno, coords.join(';'));
|
||||
},
|
||||
reset_drag_xy : function (choiceno) {
|
||||
this.set_form_value(choiceno, '');
|
||||
},
|
||||
set_form_value : function (choiceno, value) {
|
||||
this.doc.input_for_choice(choiceno).set('value', value);
|
||||
},
|
||||
//make sure xy value is not out of bounds of bg image
|
||||
xy_in_bgimg : function (bgimgxy) {
|
||||
if ((bgimgxy[0] < 0) ||
|
||||
(bgimgxy[1] < 0) ||
|
||||
(bgimgxy[0] > this.doc.bg_img().get('width')) ||
|
||||
(bgimgxy[1] > this.doc.bg_img().get('height'))){
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
constrain_to_bgimg : function (windowxy) {
|
||||
var bgimgxy = this.convert_to_bg_img_xy(windowxy);
|
||||
bgimgxy[0] = Math.max(0, bgimgxy[0]);
|
||||
bgimgxy[1] = Math.max(0, bgimgxy[1]);
|
||||
bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
|
||||
bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
|
||||
return this.convert_to_window_xy(bgimgxy);
|
||||
},
|
||||
convert_to_bg_img_xy : function (windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
redraw_drags_and_drops : function() {
|
||||
this.doc.drag_items().each(function(item) {
|
||||
//if (!item.hasClass('beingdragged')){
|
||||
item.addClass('unneeded');
|
||||
//}
|
||||
}, this);
|
||||
this.doc.inputs_for_choices().each(function (input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var coords = this.get_coords(input);
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (!dragitem || dragitem.hasClass('beingdragged')) {
|
||||
dragitem = this.clone_new_drag_item(dragitemhome, i);
|
||||
} else {
|
||||
dragitem.removeClass('unneeded');
|
||||
}
|
||||
dragitem.setXY(coords[i]);
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(item) {
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove(true);
|
||||
}
|
||||
}, this);
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.clear();
|
||||
} else {
|
||||
this.graphics = new Y.Graphic(
|
||||
{render:this.doc.top_node().one("div.ddarea div.dropzones")}
|
||||
);
|
||||
}
|
||||
if (this.get('dropzones').length !== 0) {
|
||||
this.restart_colours();
|
||||
for (var dropzoneno in this.get('dropzones')) {
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
var d = this.get('dropzones')[dropzoneno];
|
||||
this.draw_drop_zone(dropzoneno, d.markertext,
|
||||
d.shape, d.coords, colourfordropzone, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*/
|
||||
get_coords : function (input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var fv = input.get('value');
|
||||
var infinite = input.hasClass('infinite');
|
||||
var noofdrags = this.get_noofdrags_for_node(input);
|
||||
var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
|
||||
var coords = [];
|
||||
if (fv !== '') {
|
||||
var coordsstrings = fv.split(';');
|
||||
for (var i = 0; i < coordsstrings.length; i++) {
|
||||
coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayeddrags < noofdrags)) {
|
||||
coords[coords.length] = this.drag_home_xy(choiceno);
|
||||
}
|
||||
return coords;
|
||||
},
|
||||
drag_home_xy : function (choiceno) {
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
return [dragitemhome.getX(), dragitemhome.getY() - 12];
|
||||
},
|
||||
get_choiceno_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
|
||||
},
|
||||
get_itemno_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
|
||||
},
|
||||
get_noofdrags_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
|
||||
},
|
||||
|
||||
// Keyboard accessibility stuff below here.
|
||||
drop_zone_key_press : function (e) {
|
||||
var dragitem = e.target;
|
||||
var xy = dragitem.getXY();
|
||||
switch (e.direction) {
|
||||
case 'left' :
|
||||
xy[0] -= 1;
|
||||
break;
|
||||
case 'right' :
|
||||
xy[0] += 1;
|
||||
break;
|
||||
case 'down' :
|
||||
xy[1] += 1;
|
||||
break;
|
||||
case 'up' :
|
||||
xy[1] -= 1;
|
||||
break;
|
||||
case 'remove' :
|
||||
xy = null;
|
||||
break;
|
||||
}
|
||||
var choiceno = this.get_choiceno_for_node(dragitem);
|
||||
if (xy !== null) {
|
||||
xy = this.constrain_to_bgimg(xy);
|
||||
} else {
|
||||
xy = this.drag_home_xy(choiceno);
|
||||
}
|
||||
e.preventDefault();
|
||||
dragitem.setXY(xy);
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
}
|
||||
}, {NAME : DDMARKERQUESTIONNAME, ATTRS : {dropzones:{value:[]}}});
|
||||
|
||||
Y.Event.define('dragchange', {
|
||||
// Webkit and IE repeat keydown when you hold down arrow keys.
|
||||
// Opera links keypress to page scroll; others keydown.
|
||||
// Firefox prevents page scroll via preventDefault() on either
|
||||
// keydown or keypress.
|
||||
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
|
||||
|
||||
_keys: {
|
||||
'32': 'remove', // Space
|
||||
'37': 'left', // Left arrow
|
||||
'38': 'up', // Up arrow
|
||||
'39': 'right', // Right arrow
|
||||
'40': 'down', // Down arrow
|
||||
'65': 'left', // a
|
||||
'87': 'up', // w
|
||||
'68': 'right', // d
|
||||
'83': 'down', // s
|
||||
'27': 'remove' // Escape
|
||||
},
|
||||
|
||||
_keyHandler: function (e, notifier) {
|
||||
if (this._keys[e.keyCode]) {
|
||||
e.direction = this._keys[e.keyCode];
|
||||
notifier.fire(e);
|
||||
}
|
||||
},
|
||||
|
||||
on: function (node, sub, notifier) {
|
||||
sub._detacher = node.on(this._event, this._keyHandler,
|
||||
this, notifier);
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker.init_question = function(config) {
|
||||
return new DDMARKER_QUESTION(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["node", "event-resize", "dd", "dd-drop", "dd-constrain", "graphics"]});
|
||||
@@ -0,0 +1,607 @@
|
||||
YUI.add('moodle-qtype_ddmarker-dd', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
var DDMARKERDDNAME = 'moodle-qtype_ddmarker-dd';
|
||||
var DDMARKER_DD = function() {
|
||||
DDMARKER_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDMARKER_DD, Y.Base, {
|
||||
doc : null,
|
||||
polltimer : null,
|
||||
afterimageloaddone : false,
|
||||
graphics : null,
|
||||
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
if (bgdone) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure : function () {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node : function() {
|
||||
return topnode;
|
||||
},
|
||||
bg_img : function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img : function (url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
drag_items : function() {
|
||||
return dragitemsarea.all('.dragitem');
|
||||
},
|
||||
drag_items_for_choice : function(choiceno) {
|
||||
return dragitemsarea.all('span.dragitem.choice' + choiceno);
|
||||
},
|
||||
drag_item_for_choice : function(choiceno, itemno) {
|
||||
return dragitemsarea.one('span.dragitem.choice' + choiceno +
|
||||
'.item' + itemno);
|
||||
},
|
||||
drag_item_being_dragged : function(choiceno) {
|
||||
return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
|
||||
},
|
||||
drag_item_home : function (choiceno) {
|
||||
return dragitemsarea.one('span.draghome.choice' + choiceno);
|
||||
},
|
||||
drag_item_homes : function() {
|
||||
return dragitemsarea.all('span.draghome');
|
||||
},
|
||||
get_classname_numeric_suffix : function(node, prefix) {
|
||||
var classes = node.getAttribute('class');
|
||||
if (classes !== '') {
|
||||
var classesarr = classes.split(' ');
|
||||
for (var index = 0; index < classesarr.length; index++) {
|
||||
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
|
||||
if (patt1.test(classesarr[index])) {
|
||||
var patt2 = new RegExp('([0-9])+$');
|
||||
var match = patt2.exec(classesarr[index]);
|
||||
return Number(match[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputs_for_choices : function () {
|
||||
return topnode.all('input.choices');
|
||||
},
|
||||
input_for_choice : function (choiceno) {
|
||||
return topnode.one('input.choice' + choiceno);
|
||||
},
|
||||
marker_texts : function () {
|
||||
return topnode.one('div.markertexts');
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
colours : ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
|
||||
'#87CEFA','#DAA520', '#FFD700', '#F0E68C'],
|
||||
nextcolourindex : 0,
|
||||
restart_colours : function () {
|
||||
this.nextcolourindex = 0;
|
||||
},
|
||||
get_next_colour : function () {
|
||||
var colour = this.colours[this.nextcolourindex];
|
||||
this.nextcolourindex++;
|
||||
if (this.nextcolourindex === this.colours.length) {
|
||||
this.nextcolourindex = 0;
|
||||
}
|
||||
return colour;
|
||||
},
|
||||
convert_to_window_xy : function (bgimgxy) {
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
},
|
||||
shapes : [],
|
||||
draw_drop_zone : function (dropzoneno, markertext, shape, coords, colour, link) {
|
||||
var existingmarkertext;
|
||||
if (link) {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
|
||||
} else {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
|
||||
}
|
||||
|
||||
if (existingmarkertext) {
|
||||
if (markertext !== '') {
|
||||
existingmarkertext.setContent(markertext);
|
||||
} else {
|
||||
existingmarkertext.remove(true);
|
||||
}
|
||||
} else if (markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropzoneno;
|
||||
if (link) {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
|
||||
markertext + '</a></span>');
|
||||
} else {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '">' +
|
||||
markertext + '</span>');
|
||||
}
|
||||
}
|
||||
var drawfunc = 'draw_shape_' + shape;
|
||||
if (this[drawfunc] instanceof Function){
|
||||
var xyfortext = this[drawfunc](dropzoneno, coords, colour);
|
||||
if (xyfortext !== null) {
|
||||
var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
|
||||
if (markerspan !== null) {
|
||||
markerspan.setStyle('opacity', '0.6');
|
||||
xyfortext[0] -= markerspan.get('offsetWidth') / 2;
|
||||
xyfortext[1] -= markerspan.get('offsetHeight') / 2;
|
||||
markerspan.setXY(this.convert_to_window_xy(xyfortext));
|
||||
var markerspananchor = markerspan.one('a');
|
||||
if (markerspananchor !== null) {
|
||||
markerspananchor.once('click', function (e, dropzoneno) {
|
||||
var fill = this.shapes[dropzoneno].get('fill');
|
||||
fill.opacity = 1;
|
||||
this.shapes[dropzoneno].set('fill', fill);
|
||||
},
|
||||
this,
|
||||
dropzoneno
|
||||
);
|
||||
markerspananchor.set('tabIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
draw_shape_circle : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 4) {
|
||||
var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
|
||||
if (this.coords_in_img(xy)) {
|
||||
var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'circle',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
draw_shape_rectangle : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 5) {
|
||||
var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
|
||||
if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'rect',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
},
|
||||
draw_shape_polygon : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.split(';');
|
||||
var xy = [];
|
||||
for (var i in coordsparts) {
|
||||
var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
|
||||
if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
|
||||
xy[xy.length] = [parts[1], parts[2]];
|
||||
}
|
||||
}
|
||||
if (xy.length > 2) {
|
||||
var polygon = this.graphics.addShape({
|
||||
type: "path",
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
},
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity : "0.5"
|
||||
}
|
||||
});
|
||||
var maxxy = [0,0];
|
||||
var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
|
||||
for (i = 0; i < xy.length; i++) {
|
||||
//calculate min and max points to find center to show marker on
|
||||
minxy[0] = Math.min(xy[i][0], minxy[0]);
|
||||
minxy[1] = Math.min(xy[i][1], minxy[1]);
|
||||
maxxy[0] = Math.max(xy[i][0], maxxy[0]);
|
||||
maxxy[1] = Math.max(xy[i][1], maxxy[1]);
|
||||
if (i === 0) {
|
||||
polygon.moveTo(xy[i][0], xy[i][1]);
|
||||
} else {
|
||||
polygon.lineTo(xy[i][0], xy[i][1]);
|
||||
}
|
||||
}
|
||||
if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
|
||||
polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
|
||||
}
|
||||
polygon.end();
|
||||
polygon.setXY(this.doc.bg_img().getXY());
|
||||
this.shapes[dropzoneno] = polygon;
|
||||
return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
coords_in_img : function (coords) {
|
||||
return (coords[0] <= this.doc.bg_img().get('width') &&
|
||||
coords[1] <= this.doc.bg_img().get('height'));
|
||||
}
|
||||
}, {
|
||||
NAME : DDMARKERDDNAME,
|
||||
ATTRS : {
|
||||
drops : {value : null},
|
||||
readonly : {value : false},
|
||||
topnode : {value : null}
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
|
||||
|
||||
var DDMARKERQUESTIONNAME = 'ddmarker_question';
|
||||
var DDMARKER_QUESTION = function() {
|
||||
DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
|
||||
touchscrolldisable: null,
|
||||
pendingid: '',
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddmarker-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.after_image_load);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.after_image_load);
|
||||
},
|
||||
after_image_load : function () {
|
||||
this.redraw_drags_and_drops();
|
||||
M.util.js_complete(this.pendingid);
|
||||
Y.later(2000, this, this.redraw_drags_and_drops, [], true);
|
||||
},
|
||||
clone_new_drag_item : function (draghome, itemno) {
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('dragitem');
|
||||
drag.addClass('item' + itemno);
|
||||
drag.one('span.markertext').setStyle('opacity', 0.6);
|
||||
draghome.insert(drag, 'after');
|
||||
if (!this.get('readonly')) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
return drag;
|
||||
},
|
||||
|
||||
/**
|
||||
* prevent_touchmove_from_scrolling allows users of touch screen devices to
|
||||
* use drag and drop and normal scrolling at the same time. I.e.when
|
||||
* touching and dragging a draggable item, the screen does not scroll, but
|
||||
* you can scroll by touching other area of the screen apart from the
|
||||
* draggable items.
|
||||
*/
|
||||
prevent_touchmove_from_scrolling : function(drag) {
|
||||
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
|
||||
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
|
||||
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
|
||||
|
||||
// Disable scrolling when touching the draggable items.
|
||||
drag.on(touchstart, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
return; // Already disabled.
|
||||
}
|
||||
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
});
|
||||
}, this);
|
||||
|
||||
// Allow scrolling after releasing the draggable items.
|
||||
drag.on(touchend, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
this.touchscrolldisable.detach();
|
||||
this.touchscrolldisable = null;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
draggable : function (drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'intersect'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
|
||||
dd.after('drag:start', function(e){
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.addClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
var itemno = this.get_itemno_for_node(dragnode);
|
||||
if (itemno !== null) {
|
||||
dragnode.removeClass('item' + dragnode);
|
||||
}
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
dd.after('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.removeClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
this.save_all_xy_for_choice(choiceno, dragnode);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
//--- keyboard accessibility
|
||||
drag.set('tabIndex', 0);
|
||||
drag.on('dragchange', this.drop_zone_key_press, this);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(drag);
|
||||
},
|
||||
|
||||
save_all_xy_for_choice: function (choiceno, dropped) {
|
||||
var coords = [];
|
||||
var bgimgxy;
|
||||
for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (dragitem) {
|
||||
dragitem.removeClass('item' + i);
|
||||
if (!dragitem.hasClass('beingdragged')) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
dragitem.removeClass('item' + i);
|
||||
dragitem.addClass('item' + coords.length);
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped !== null){
|
||||
bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
|
||||
dropped.addClass('item' + coords.length);
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
this.set_form_value(choiceno, coords.join(';'));
|
||||
},
|
||||
reset_drag_xy : function (choiceno) {
|
||||
this.set_form_value(choiceno, '');
|
||||
},
|
||||
set_form_value : function (choiceno, value) {
|
||||
this.doc.input_for_choice(choiceno).set('value', value);
|
||||
},
|
||||
//make sure xy value is not out of bounds of bg image
|
||||
xy_in_bgimg : function (bgimgxy) {
|
||||
if ((bgimgxy[0] < 0) ||
|
||||
(bgimgxy[1] < 0) ||
|
||||
(bgimgxy[0] > this.doc.bg_img().get('width')) ||
|
||||
(bgimgxy[1] > this.doc.bg_img().get('height'))){
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
constrain_to_bgimg : function (windowxy) {
|
||||
var bgimgxy = this.convert_to_bg_img_xy(windowxy);
|
||||
bgimgxy[0] = Math.max(0, bgimgxy[0]);
|
||||
bgimgxy[1] = Math.max(0, bgimgxy[1]);
|
||||
bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
|
||||
bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
|
||||
return this.convert_to_window_xy(bgimgxy);
|
||||
},
|
||||
convert_to_bg_img_xy : function (windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
redraw_drags_and_drops : function() {
|
||||
this.doc.drag_items().each(function(item) {
|
||||
//if (!item.hasClass('beingdragged')){
|
||||
item.addClass('unneeded');
|
||||
//}
|
||||
}, this);
|
||||
this.doc.inputs_for_choices().each(function (input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var coords = this.get_coords(input);
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (!dragitem || dragitem.hasClass('beingdragged')) {
|
||||
dragitem = this.clone_new_drag_item(dragitemhome, i);
|
||||
} else {
|
||||
dragitem.removeClass('unneeded');
|
||||
}
|
||||
dragitem.setXY(coords[i]);
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(item) {
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove(true);
|
||||
}
|
||||
}, this);
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.clear();
|
||||
} else {
|
||||
this.graphics = new Y.Graphic(
|
||||
{render:this.doc.top_node().one("div.ddarea div.dropzones")}
|
||||
);
|
||||
}
|
||||
if (this.get('dropzones').length !== 0) {
|
||||
this.restart_colours();
|
||||
for (var dropzoneno in this.get('dropzones')) {
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
var d = this.get('dropzones')[dropzoneno];
|
||||
this.draw_drop_zone(dropzoneno, d.markertext,
|
||||
d.shape, d.coords, colourfordropzone, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*/
|
||||
get_coords : function (input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var fv = input.get('value');
|
||||
var infinite = input.hasClass('infinite');
|
||||
var noofdrags = this.get_noofdrags_for_node(input);
|
||||
var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
|
||||
var coords = [];
|
||||
if (fv !== '') {
|
||||
var coordsstrings = fv.split(';');
|
||||
for (var i = 0; i < coordsstrings.length; i++) {
|
||||
coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayeddrags < noofdrags)) {
|
||||
coords[coords.length] = this.drag_home_xy(choiceno);
|
||||
}
|
||||
return coords;
|
||||
},
|
||||
drag_home_xy : function (choiceno) {
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
return [dragitemhome.getX(), dragitemhome.getY() - 12];
|
||||
},
|
||||
get_choiceno_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
|
||||
},
|
||||
get_itemno_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
|
||||
},
|
||||
get_noofdrags_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
|
||||
},
|
||||
|
||||
// Keyboard accessibility stuff below here.
|
||||
drop_zone_key_press : function (e) {
|
||||
var dragitem = e.target;
|
||||
var xy = dragitem.getXY();
|
||||
switch (e.direction) {
|
||||
case 'left' :
|
||||
xy[0] -= 1;
|
||||
break;
|
||||
case 'right' :
|
||||
xy[0] += 1;
|
||||
break;
|
||||
case 'down' :
|
||||
xy[1] += 1;
|
||||
break;
|
||||
case 'up' :
|
||||
xy[1] -= 1;
|
||||
break;
|
||||
case 'remove' :
|
||||
xy = null;
|
||||
break;
|
||||
}
|
||||
var choiceno = this.get_choiceno_for_node(dragitem);
|
||||
if (xy !== null) {
|
||||
xy = this.constrain_to_bgimg(xy);
|
||||
} else {
|
||||
xy = this.drag_home_xy(choiceno);
|
||||
}
|
||||
e.preventDefault();
|
||||
dragitem.setXY(xy);
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
}
|
||||
}, {NAME : DDMARKERQUESTIONNAME, ATTRS : {dropzones:{value:[]}}});
|
||||
|
||||
Y.Event.define('dragchange', {
|
||||
// Webkit and IE repeat keydown when you hold down arrow keys.
|
||||
// Opera links keypress to page scroll; others keydown.
|
||||
// Firefox prevents page scroll via preventDefault() on either
|
||||
// keydown or keypress.
|
||||
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
|
||||
|
||||
_keys: {
|
||||
'32': 'remove', // Space
|
||||
'37': 'left', // Left arrow
|
||||
'38': 'up', // Up arrow
|
||||
'39': 'right', // Right arrow
|
||||
'40': 'down', // Down arrow
|
||||
'65': 'left', // a
|
||||
'87': 'up', // w
|
||||
'68': 'right', // d
|
||||
'83': 'down', // s
|
||||
'27': 'remove' // Escape
|
||||
},
|
||||
|
||||
_keyHandler: function (e, notifier) {
|
||||
if (this._keys[e.keyCode]) {
|
||||
e.direction = this._keys[e.keyCode];
|
||||
notifier.fire(e);
|
||||
}
|
||||
},
|
||||
|
||||
on: function (node, sub, notifier) {
|
||||
sub._detacher = node.on(this._event, this._keyHandler,
|
||||
this, notifier);
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker.init_question = function(config) {
|
||||
return new DDMARKER_QUESTION(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["node", "event-resize", "dd", "dd-drop", "dd-constrain", "graphics"]});
|
||||
@@ -0,0 +1,277 @@
|
||||
YUI.add('moodle-qtype_ddmarker-form', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDMARKERFORMNAME = 'moodle-qtype_ddmarker-form';
|
||||
var DDMARKER_FORM = function() {
|
||||
DDMARKER_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
|
||||
fp : null,
|
||||
|
||||
initializer : function() {
|
||||
var pendingid = 'qtype_ddmarker-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append(
|
||||
'<div class="ddarea">' +
|
||||
'<div class="markertexts"></div>' +
|
||||
'<div class="droparea"></div>' +
|
||||
'<div class="dropzones"></div>' +
|
||||
'<div class="grid"></div>' +
|
||||
'</div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.update_drop_zones, [pendingid], true);
|
||||
Y.after(this.load_bg_image, M.form_filepicker, 'callback', this);
|
||||
this.load_bg_image();
|
||||
},
|
||||
|
||||
load_bg_image : function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
|
||||
// Listen for a drop:hit on the background image.
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this);
|
||||
}
|
||||
},
|
||||
|
||||
constrain_image_size : function (e) {
|
||||
var maxsize = this.get('maxsizes').bgimage;
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
update_drop_zones : function (pendingid) {
|
||||
|
||||
// Set up drop zones.
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.destroy();
|
||||
}
|
||||
this.restart_colours();
|
||||
this.graphics = new Y.Graphic({render:"div.ddarea div.dropzones"});
|
||||
var noofdropzones = this.form.get_form_value('nodropzone', []);
|
||||
for (var dropzoneno = 0; dropzoneno < noofdropzones; dropzoneno++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [dropzoneno, 'choice']);
|
||||
var markertext = this.get_marker_text(dragitemno);
|
||||
var shape = this.form.get_form_value('drops', [dropzoneno, 'shape']);
|
||||
var coords = this.get_coords(dropzoneno);
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
Y.one('input#id_drops_' + dropzoneno + '_coords')
|
||||
.setStyle('background-color', colourfordropzone);
|
||||
this.draw_drop_zone(dropzoneno, markertext,
|
||||
shape, coords, colourfordropzone, false);
|
||||
}
|
||||
if (this.doc.bg_img()) {
|
||||
Y.one('div.ddarea .grid')
|
||||
.setXY(this.doc.bg_img().getXY())
|
||||
.setStyle('width', this.doc.bg_img().get('width'))
|
||||
.setStyle('height', this.doc.bg_img().get('height'));
|
||||
}
|
||||
M.util.js_complete(pendingid);
|
||||
},
|
||||
|
||||
get_coords : function (dropzoneno) {
|
||||
var coords = this.form.get_form_value('drops', [dropzoneno, 'coords']);
|
||||
return coords.replace(new RegExp("\\s*", 'g'), '');
|
||||
},
|
||||
get_marker_text : function (markerno) {
|
||||
if (Number(markerno) !== 0) {
|
||||
var label = this.form.get_form_value('drags', [markerno - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors : function () {
|
||||
var dragitemsoptions = {0: ''};
|
||||
for (var i = 1; i <= this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.get_marker_text(i);
|
||||
if (label !== "") {
|
||||
dragitemsoptions[i] = Y.Escape.html(label);
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
var selectedvalues = [];
|
||||
var selector;
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
selectedvalues[i] = Number(selector.get('value'));
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.all('option').remove(true);
|
||||
// And recreate the options.
|
||||
for (var value in dragitemsoptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedvalues[i]) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
// It is not the currently selected value, is it selectable?
|
||||
if (value !== 0) { // The 'no item' option is always selectable.
|
||||
// Variables to hold form values about this drag item.
|
||||
var noofdrags = this.form.get_form_value('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) !== 0) { // 'noofdrags == 0' means infinite.
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedvalues) {
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(selectedvalues[k]) === value) {
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.set('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events : function () {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events : function () {
|
||||
//events triggered by changes to form data
|
||||
|
||||
// Changes to labels.
|
||||
Y.all('fieldset#id_draggableitemheader input').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Changes to selected drag item.
|
||||
Y.all('fieldset#id_draggableitemheader select').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form : {
|
||||
to_name_with_index : function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el : function (name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value : function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value : function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index : function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers : function () {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file : function (name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href : null, name : null};
|
||||
}
|
||||
},
|
||||
name : function (draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
},{NAME : DDMARKERFORMNAME, ATTRS : {maxsizes:{value:null}}});
|
||||
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.init_form = function(config) {
|
||||
return new DDMARKER_FORM(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["moodle-qtype_ddmarker-dd", "form_filepicker", "graphics", "escape"]});
|
||||
@@ -0,0 +1 @@
|
||||
YUI.add("moodle-qtype_ddmarker-form",function(e,t){var n="moodle-qtype_ddmarker-form",r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,M.qtype_ddmarker.dd_base_class,{fp:null,initializer:function(){var t="qtype_ddmarker-form-"+Math.random().toString(36).slice(2);M.util.js_pending(t),this.fp=this.file_pickers();var n=e.one(this.get("topnode"));n.one("div.fcontainer").append('<div class="ddarea"><div class="markertexts"></div><div class="droparea"></div><div class="dropzones"></div><div class="grid"></div></div>'),this.doc=this.doc_structure(this),this.stop_selector_events(),this.set_options_for_drag_item_selectors(),this.setup_form_events(),e.later(500,this,this.update_drop_zones,[t],!0),e.after(this.load_bg_image,M.form_filepicker,"callback",this),this.load_bg_image()},load_bg_image:function(){var t=this.fp.file("bgimage").href;if(t!==null){this.doc.load_bg_img(t);var n=new e.DD.Drop({node:this.doc.bg_img()});n.on("drop:hit",function(e){e.drag.get("node").setData("gooddrop",!0)}),this.afterimageloaddone=!1,this.doc.bg_img().on("load",this.constrain_image_size,this)}},constrain_image_size:function(e){var t=this.get("maxsizes").bgimage,n=Math.max(e.target.get("width")/t.width,e.target.get("height")/t.height);n>1&&e.target.set("width",Math.floor(e.target.get("width")/n)),e.target.addClass("constrained"),e.target.detach("load",this.constrain_image_size)},update_drop_zones:function(t){this.graphics!==null&&this.graphics.destroy(),this.restart_colours(),this.graphics=new e.Graphic({render:"div.ddarea div.dropzones"});var n=this.form.get_form_value("nodropzone",[]);for(var r=0;r<n;r++){var i=this.form.get_form_value("drops",[r,"choice"]),s=this.get_marker_text(i),o=this.form.get_form_value("drops",[r,"shape"]),u=this.get_coords(r),a=this.get_next_colour();e.one("input#id_drops_"+r+"_coords").setStyle("background-color",a),this.draw_drop_zone(r,s,o,u,a,!1)}this.doc.bg_img()&&e.one("div.ddarea .grid").setXY(this.doc.bg_img().getXY()).setStyle("width",this.doc.bg_img().get("width")).setStyle("height",this.doc.bg_img().get("height")),M.util.js_complete(t)},get_coords:function(e){var t=this.form.get_form_value("drops",[e,"coords"]);return t.replace(new RegExp("\\s*","g"),"")},get_marker_text:function(e){if(Number(e)!==0){var t=this.form.get_form_value("drags",[e-1,"label"]);return t.replace(new RegExp("^\\s*(.*)\\s*$"),"$1")}return""},set_options_for_drag_item_selectors:function(){var t={0:""};for(var n=1;n<=this.form.get_form_value("noitems",[]);n++){var r=this.get_marker_text(n);r!==""&&(t[n]=e.Escape.html(r))}var i=[],s;for(n=0;n<this.form.get_form_value("nodropzone",[]);n++)s=e.one("#id_drops_"+n+"_choice"),i[n]=Number(s.get("value"));for(n=0;n<this.form.get_form_value("nodropzone",[]);n++){s=e.one("#id_drops_"+n+"_choice"),s.all("option").remove(!0);for(var o in t){o=Number(o);var u='<option value="'+o+'">'+t[o]+"</option>";s.append(u);var a=s.one('option[value="'+o+'"]');if(o===i[n])a.set("selected",!0);else if(o!==0){var f=this.form.get_form_value("drags",[o-1,"noofdrags"]);if(Number(f)!==0)for(var l in i)if(Number(i[l])===o){if(Number(f)===1){a.set("disabled",!0);break}f--}}}}},stop_selector_events:function(){e.all("fieldset#id_dropzoneheader select").detachAll()},setup_form_events:function(){e.all("fieldset#id_draggableitemheader input").on("change",function(){this.set_options_for_drag_item_selectors()},this),e.all("fieldset#id_draggableitemheader select").on("change",function(){this.set_options_for_drag_item_selectors()},this),e.all("fieldset#id_dropzoneheader select").on("change",function(){this.set_options_for_drag_item_selectors()},this)},form:{to_name_with_index:function(e,t){var n=e;for(var r=0;r<t.length;r++)n=n+"["+t[r]+"]";return n},get_el:function(e,t){var n=document.getElementById("mform1");return n.elements[this.to_name_with_index(e,t)]},get_form_value:function(e,t){var n=this.get_el(e,t);return n.type==="checkbox"?n.checked:n.value},set_form_value:function(e,t,n){var r=this.get_el(e,t);r.type==="checkbox"?r.checked=n:r.value=n},from_name_with_index:function(e){var t={};t.indexes=[];var n=e.indexOf("[");t.name=e.substring(0,n);while(n!==-1){var r=e.indexOf("]",n+1);t.indexes.push(e.substring(n+1,r)),n=e.indexOf("[",r+1)}return t}},file_pickers:function(){var t,n;if(t===undefined){t={},n={};var r=e.all("form.mform input.filepickerhidden");r.each(function(e){t[e.get("value")]=e.get("name"),n[e.get("name")]=e.get("parentNode")},this)}var i={file:function(e){var t=n[e],r=t.one("div.filepicker-filelist a");return r?{href:r.get("href"),name:r.get("innerHTML")}:{href:null,name:null}},name:function(e){return t[e]}};return i}},{NAME:n,ATTRS:{maxsizes:{value:null}}}),M.qtype_ddmarker=M.qtype_ddmarker||{},M.qtype_ddmarker.init_form=function(e){return new r(e)}},"@VERSION@",{requires:["moodle-qtype_ddmarker-dd","form_filepicker","graphics","escape"]});
|
||||
@@ -0,0 +1,277 @@
|
||||
YUI.add('moodle-qtype_ddmarker-form', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDMARKERFORMNAME = 'moodle-qtype_ddmarker-form';
|
||||
var DDMARKER_FORM = function() {
|
||||
DDMARKER_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
|
||||
fp : null,
|
||||
|
||||
initializer : function() {
|
||||
var pendingid = 'qtype_ddmarker-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append(
|
||||
'<div class="ddarea">' +
|
||||
'<div class="markertexts"></div>' +
|
||||
'<div class="droparea"></div>' +
|
||||
'<div class="dropzones"></div>' +
|
||||
'<div class="grid"></div>' +
|
||||
'</div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.update_drop_zones, [pendingid], true);
|
||||
Y.after(this.load_bg_image, M.form_filepicker, 'callback', this);
|
||||
this.load_bg_image();
|
||||
},
|
||||
|
||||
load_bg_image : function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
|
||||
// Listen for a drop:hit on the background image.
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this);
|
||||
}
|
||||
},
|
||||
|
||||
constrain_image_size : function (e) {
|
||||
var maxsize = this.get('maxsizes').bgimage;
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
update_drop_zones : function (pendingid) {
|
||||
|
||||
// Set up drop zones.
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.destroy();
|
||||
}
|
||||
this.restart_colours();
|
||||
this.graphics = new Y.Graphic({render:"div.ddarea div.dropzones"});
|
||||
var noofdropzones = this.form.get_form_value('nodropzone', []);
|
||||
for (var dropzoneno = 0; dropzoneno < noofdropzones; dropzoneno++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [dropzoneno, 'choice']);
|
||||
var markertext = this.get_marker_text(dragitemno);
|
||||
var shape = this.form.get_form_value('drops', [dropzoneno, 'shape']);
|
||||
var coords = this.get_coords(dropzoneno);
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
Y.one('input#id_drops_' + dropzoneno + '_coords')
|
||||
.setStyle('background-color', colourfordropzone);
|
||||
this.draw_drop_zone(dropzoneno, markertext,
|
||||
shape, coords, colourfordropzone, false);
|
||||
}
|
||||
if (this.doc.bg_img()) {
|
||||
Y.one('div.ddarea .grid')
|
||||
.setXY(this.doc.bg_img().getXY())
|
||||
.setStyle('width', this.doc.bg_img().get('width'))
|
||||
.setStyle('height', this.doc.bg_img().get('height'));
|
||||
}
|
||||
M.util.js_complete(pendingid);
|
||||
},
|
||||
|
||||
get_coords : function (dropzoneno) {
|
||||
var coords = this.form.get_form_value('drops', [dropzoneno, 'coords']);
|
||||
return coords.replace(new RegExp("\\s*", 'g'), '');
|
||||
},
|
||||
get_marker_text : function (markerno) {
|
||||
if (Number(markerno) !== 0) {
|
||||
var label = this.form.get_form_value('drags', [markerno - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors : function () {
|
||||
var dragitemsoptions = {0: ''};
|
||||
for (var i = 1; i <= this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.get_marker_text(i);
|
||||
if (label !== "") {
|
||||
dragitemsoptions[i] = Y.Escape.html(label);
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
var selectedvalues = [];
|
||||
var selector;
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
selectedvalues[i] = Number(selector.get('value'));
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.all('option').remove(true);
|
||||
// And recreate the options.
|
||||
for (var value in dragitemsoptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedvalues[i]) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
// It is not the currently selected value, is it selectable?
|
||||
if (value !== 0) { // The 'no item' option is always selectable.
|
||||
// Variables to hold form values about this drag item.
|
||||
var noofdrags = this.form.get_form_value('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) !== 0) { // 'noofdrags == 0' means infinite.
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedvalues) {
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(selectedvalues[k]) === value) {
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.set('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events : function () {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events : function () {
|
||||
//events triggered by changes to form data
|
||||
|
||||
// Changes to labels.
|
||||
Y.all('fieldset#id_draggableitemheader input').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Changes to selected drag item.
|
||||
Y.all('fieldset#id_draggableitemheader select').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form : {
|
||||
to_name_with_index : function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el : function (name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value : function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value : function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index : function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers : function () {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file : function (name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href : null, name : null};
|
||||
}
|
||||
},
|
||||
name : function (draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
},{NAME : DDMARKERFORMNAME, ATTRS : {maxsizes:{value:null}}});
|
||||
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.init_form = function(config) {
|
||||
return new DDMARKER_FORM(config);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["moodle-qtype_ddmarker-dd", "form_filepicker", "graphics", "escape"]});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "moodle-qtype_ddmarker-dd",
|
||||
"builds": {
|
||||
"moodle-qtype_ddmarker-dd": {
|
||||
"jsfiles": [
|
||||
"ddmarker.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
// 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/>.
|
||||
|
||||
var DDMARKERDDNAME = 'moodle-qtype_ddmarker-dd';
|
||||
var DDMARKER_DD = function() {
|
||||
DDMARKER_DD.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the base class for the question rendering and question editing form code.
|
||||
*/
|
||||
Y.extend(DDMARKER_DD, Y.Base, {
|
||||
doc : null,
|
||||
polltimer : null,
|
||||
afterimageloaddone : false,
|
||||
graphics : null,
|
||||
poll_for_image_load : function (e, waitforimageconstrain, pause, doafterwords) {
|
||||
if (this.afterimageloaddone) {
|
||||
return;
|
||||
}
|
||||
var bgdone = this.doc.bg_img().get('complete');
|
||||
if (waitforimageconstrain) {
|
||||
bgdone = bgdone && this.doc.bg_img().hasClass('constrained');
|
||||
}
|
||||
if (bgdone) {
|
||||
if (this.polltimer !== null) {
|
||||
this.polltimer.cancel();
|
||||
this.polltimer = null;
|
||||
}
|
||||
this.doc.bg_img().detach('load', this.poll_for_image_load);
|
||||
if (pause !== 0) {
|
||||
Y.later(pause, this, doafterwords);
|
||||
} else {
|
||||
doafterwords.call(this);
|
||||
}
|
||||
this.afterimageloaddone = true;
|
||||
} else if (this.polltimer === null) {
|
||||
var pollarguments = [null, waitforimageconstrain, pause, doafterwords];
|
||||
this.polltimer =
|
||||
Y.later(1000, this, this.poll_for_image_load, pollarguments, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Object to encapsulate operations on dd area.
|
||||
*/
|
||||
doc_structure : function () {
|
||||
var topnode = Y.one(this.get('topnode'));
|
||||
var dragitemsarea = topnode.one('div.dragitems');
|
||||
var dropbgarea = topnode.one('div.droparea');
|
||||
return {
|
||||
top_node : function() {
|
||||
return topnode;
|
||||
},
|
||||
bg_img : function() {
|
||||
return topnode.one('.dropbackground');
|
||||
},
|
||||
load_bg_img : function (url) {
|
||||
dropbgarea.setContent('<img class="dropbackground" src="' + url + '"/>');
|
||||
this.bg_img().on('load', this.on_image_load, this, 'bg_image');
|
||||
},
|
||||
drag_items : function() {
|
||||
return dragitemsarea.all('.dragitem');
|
||||
},
|
||||
drag_items_for_choice : function(choiceno) {
|
||||
return dragitemsarea.all('span.dragitem.choice' + choiceno);
|
||||
},
|
||||
drag_item_for_choice : function(choiceno, itemno) {
|
||||
return dragitemsarea.one('span.dragitem.choice' + choiceno +
|
||||
'.item' + itemno);
|
||||
},
|
||||
drag_item_being_dragged : function(choiceno) {
|
||||
return dragitemsarea.one('span.dragitem.beingdragged.choice' + choiceno);
|
||||
},
|
||||
drag_item_home : function (choiceno) {
|
||||
return dragitemsarea.one('span.draghome.choice' + choiceno);
|
||||
},
|
||||
drag_item_homes : function() {
|
||||
return dragitemsarea.all('span.draghome');
|
||||
},
|
||||
get_classname_numeric_suffix : function(node, prefix) {
|
||||
var classes = node.getAttribute('class');
|
||||
if (classes !== '') {
|
||||
var classesarr = classes.split(' ');
|
||||
for (var index = 0; index < classesarr.length; index++) {
|
||||
var patt1 = new RegExp('^' + prefix + '([0-9])+$');
|
||||
if (patt1.test(classesarr[index])) {
|
||||
var patt2 = new RegExp('([0-9])+$');
|
||||
var match = patt2.exec(classesarr[index]);
|
||||
return Number(match[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputs_for_choices : function () {
|
||||
return topnode.all('input.choices');
|
||||
},
|
||||
input_for_choice : function (choiceno) {
|
||||
return topnode.one('input.choice' + choiceno);
|
||||
},
|
||||
marker_texts : function () {
|
||||
return topnode.one('div.markertexts');
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
colours : ['#FFFFFF', '#B0C4DE', '#DCDCDC', '#D8BFD8',
|
||||
'#87CEFA','#DAA520', '#FFD700', '#F0E68C'],
|
||||
nextcolourindex : 0,
|
||||
restart_colours : function () {
|
||||
this.nextcolourindex = 0;
|
||||
},
|
||||
get_next_colour : function () {
|
||||
var colour = this.colours[this.nextcolourindex];
|
||||
this.nextcolourindex++;
|
||||
if (this.nextcolourindex === this.colours.length) {
|
||||
this.nextcolourindex = 0;
|
||||
}
|
||||
return colour;
|
||||
},
|
||||
convert_to_window_xy : function (bgimgxy) {
|
||||
return [Number(bgimgxy[0]) + this.doc.bg_img().getX() + 1,
|
||||
Number(bgimgxy[1]) + this.doc.bg_img().getY() + 1];
|
||||
},
|
||||
shapes : [],
|
||||
draw_drop_zone : function (dropzoneno, markertext, shape, coords, colour, link) {
|
||||
var existingmarkertext;
|
||||
if (link) {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno + ' a');
|
||||
} else {
|
||||
existingmarkertext = this.doc.marker_texts().one('span.markertext' + dropzoneno);
|
||||
}
|
||||
|
||||
if (existingmarkertext) {
|
||||
if (markertext !== '') {
|
||||
existingmarkertext.setContent(markertext);
|
||||
} else {
|
||||
existingmarkertext.remove(true);
|
||||
}
|
||||
} else if (markertext !== '') {
|
||||
var classnames = 'markertext markertext' + dropzoneno;
|
||||
if (link) {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '"><a href="#">' +
|
||||
markertext + '</a></span>');
|
||||
} else {
|
||||
this.doc.marker_texts().append('<span class="' + classnames + '">' +
|
||||
markertext + '</span>');
|
||||
}
|
||||
}
|
||||
var drawfunc = 'draw_shape_' + shape;
|
||||
if (this[drawfunc] instanceof Function){
|
||||
var xyfortext = this[drawfunc](dropzoneno, coords, colour);
|
||||
if (xyfortext !== null) {
|
||||
var markerspan = this.doc.top_node().one('div.ddarea div.markertexts span.markertext' + dropzoneno);
|
||||
if (markerspan !== null) {
|
||||
markerspan.setStyle('opacity', '0.6');
|
||||
xyfortext[0] -= markerspan.get('offsetWidth') / 2;
|
||||
xyfortext[1] -= markerspan.get('offsetHeight') / 2;
|
||||
markerspan.setXY(this.convert_to_window_xy(xyfortext));
|
||||
var markerspananchor = markerspan.one('a');
|
||||
if (markerspananchor !== null) {
|
||||
markerspananchor.once('click', function (e, dropzoneno) {
|
||||
var fill = this.shapes[dropzoneno].get('fill');
|
||||
fill.opacity = 1;
|
||||
this.shapes[dropzoneno].set('fill', fill);
|
||||
},
|
||||
this,
|
||||
dropzoneno
|
||||
);
|
||||
markerspananchor.set('tabIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
draw_shape_circle : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 4) {
|
||||
var xy = [Number(coordsparts[1]) - coordsparts[3], Number(coordsparts[2]) - coordsparts[3]];
|
||||
if (this.coords_in_img(xy)) {
|
||||
var widthheight = [Number(coordsparts[3]) * 2, Number(coordsparts[3]) * 2];
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'circle',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
draw_shape_rectangle : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.match(/(\d+),(\d+);(\d+),(\d+)/);
|
||||
if (coordsparts && coordsparts.length === 5) {
|
||||
var xy = [Number(coordsparts[1]), Number(coordsparts[2])];
|
||||
var widthheight = [Number(coordsparts[3]), Number(coordsparts[4])];
|
||||
if (this.coords_in_img([xy[0] + widthheight[0], xy[1] + widthheight[1]])) {
|
||||
var shape = this.graphics.addShape({
|
||||
type: 'rect',
|
||||
width: widthheight[0],
|
||||
height: widthheight[1],
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity: "0.5"
|
||||
},
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
}
|
||||
});
|
||||
shape.setXY(this.convert_to_window_xy(xy));
|
||||
this.shapes[dropzoneno] = shape;
|
||||
return [Number(xy[0]) + widthheight[0] / 2, Number(xy[1]) + widthheight[1] / 2];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
},
|
||||
draw_shape_polygon : function (dropzoneno, coords, colour) {
|
||||
var coordsparts = coords.split(';');
|
||||
var xy = [];
|
||||
for (var i in coordsparts) {
|
||||
var parts = coordsparts[i].match(/^(\d+),(\d+)$/);
|
||||
if (parts !== null && this.coords_in_img([parts[1], parts[2]])) {
|
||||
xy[xy.length] = [parts[1], parts[2]];
|
||||
}
|
||||
}
|
||||
if (xy.length > 2) {
|
||||
var polygon = this.graphics.addShape({
|
||||
type: "path",
|
||||
stroke: {
|
||||
weight: 1,
|
||||
color: "black"
|
||||
},
|
||||
fill: {
|
||||
color: colour,
|
||||
opacity : "0.5"
|
||||
}
|
||||
});
|
||||
var maxxy = [0,0];
|
||||
var minxy = [this.doc.bg_img().get('width'), this.doc.bg_img().get('height')];
|
||||
for (i = 0; i < xy.length; i++) {
|
||||
//calculate min and max points to find center to show marker on
|
||||
minxy[0] = Math.min(xy[i][0], minxy[0]);
|
||||
minxy[1] = Math.min(xy[i][1], minxy[1]);
|
||||
maxxy[0] = Math.max(xy[i][0], maxxy[0]);
|
||||
maxxy[1] = Math.max(xy[i][1], maxxy[1]);
|
||||
if (i === 0) {
|
||||
polygon.moveTo(xy[i][0], xy[i][1]);
|
||||
} else {
|
||||
polygon.lineTo(xy[i][0], xy[i][1]);
|
||||
}
|
||||
}
|
||||
if (Number(xy[0][0]) !== Number(xy[xy.length - 1][0]) || Number(xy[0][1]) !== Number(xy[xy.length - 1][1])) {
|
||||
polygon.lineTo(xy[0][0], xy[0][1]); // Close polygon if not already closed.
|
||||
}
|
||||
polygon.end();
|
||||
polygon.setXY(this.doc.bg_img().getXY());
|
||||
this.shapes[dropzoneno] = polygon;
|
||||
return [(minxy[0] + maxxy[0]) / 2, (minxy[1] + maxxy[1]) / 2];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
coords_in_img : function (coords) {
|
||||
return (coords[0] <= this.doc.bg_img().get('width') &&
|
||||
coords[1] <= this.doc.bg_img().get('height'));
|
||||
}
|
||||
}, {
|
||||
NAME : DDMARKERDDNAME,
|
||||
ATTRS : {
|
||||
drops : {value : null},
|
||||
readonly : {value : false},
|
||||
topnode : {value : null}
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.dd_base_class = DDMARKER_DD;
|
||||
|
||||
var DDMARKERQUESTIONNAME = 'ddmarker_question';
|
||||
var DDMARKER_QUESTION = function() {
|
||||
DDMARKER_QUESTION.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
/**
|
||||
* This is the code for question rendering.
|
||||
*/
|
||||
Y.extend(DDMARKER_QUESTION, M.qtype_ddmarker.dd_base_class, {
|
||||
touchscrolldisable: null,
|
||||
pendingid: '',
|
||||
initializer : function() {
|
||||
this.pendingid = 'qtype_ddmarker-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(this.pendingid);
|
||||
this.doc = this.doc_structure(this);
|
||||
this.poll_for_image_load(null, false, 0, this.after_image_load);
|
||||
this.doc.bg_img().after('load', this.poll_for_image_load, this,
|
||||
false, 0, this.after_image_load);
|
||||
},
|
||||
after_image_load : function () {
|
||||
this.redraw_drags_and_drops();
|
||||
M.util.js_complete(this.pendingid);
|
||||
Y.later(2000, this, this.redraw_drags_and_drops, [], true);
|
||||
},
|
||||
clone_new_drag_item : function (draghome, itemno) {
|
||||
var drag = draghome.cloneNode(true);
|
||||
drag.removeClass('draghome');
|
||||
drag.addClass('dragitem');
|
||||
drag.addClass('item' + itemno);
|
||||
drag.one('span.markertext').setStyle('opacity', 0.6);
|
||||
draghome.insert(drag, 'after');
|
||||
if (!this.get('readonly')) {
|
||||
this.draggable(drag);
|
||||
}
|
||||
return drag;
|
||||
},
|
||||
|
||||
/**
|
||||
* prevent_touchmove_from_scrolling allows users of touch screen devices to
|
||||
* use drag and drop and normal scrolling at the same time. I.e.when
|
||||
* touching and dragging a draggable item, the screen does not scroll, but
|
||||
* you can scroll by touching other area of the screen apart from the
|
||||
* draggable items.
|
||||
*/
|
||||
prevent_touchmove_from_scrolling : function(drag) {
|
||||
var touchstart = (Y.UA.ie) ? 'MSPointerStart' : 'touchstart';
|
||||
var touchend = (Y.UA.ie) ? 'MSPointerEnd' : 'touchend';
|
||||
var touchmove = (Y.UA.ie) ? 'MSPointerMove' : 'touchmove';
|
||||
|
||||
// Disable scrolling when touching the draggable items.
|
||||
drag.on(touchstart, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
return; // Already disabled.
|
||||
}
|
||||
this.touchscrolldisable = Y.one('body').on(touchmove, function(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
});
|
||||
}, this);
|
||||
|
||||
// Allow scrolling after releasing the draggable items.
|
||||
drag.on(touchend, function() {
|
||||
if (this.touchscrolldisable) {
|
||||
this.touchscrolldisable.detach();
|
||||
this.touchscrolldisable = null;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
draggable : function (drag) {
|
||||
var dd = new Y.DD.Drag({
|
||||
node: drag,
|
||||
dragMode: 'intersect'
|
||||
}).plug(Y.Plugin.DDConstrained, {constrain2node: this.doc.top_node()});
|
||||
dd.after('drag:start', function(e){
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.addClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
var itemno = this.get_itemno_for_node(dragnode);
|
||||
if (itemno !== null) {
|
||||
dragnode.removeClass('item' + dragnode);
|
||||
}
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
dd.after('drag:end', function(e) {
|
||||
var dragnode = e.target.get('node');
|
||||
dragnode.removeClass('beingdragged');
|
||||
var choiceno = this.get_choiceno_for_node(dragnode);
|
||||
this.save_all_xy_for_choice(choiceno, dragnode);
|
||||
this.redraw_drags_and_drops();
|
||||
}, this);
|
||||
//--- keyboard accessibility
|
||||
drag.set('tabIndex', 0);
|
||||
drag.on('dragchange', this.drop_zone_key_press, this);
|
||||
|
||||
// Prevent scrolling whilst dragging on Adroid devices.
|
||||
this.prevent_touchmove_from_scrolling(drag);
|
||||
},
|
||||
|
||||
save_all_xy_for_choice: function (choiceno, dropped) {
|
||||
var coords = [];
|
||||
var bgimgxy;
|
||||
for (var i = 0; i <= this.doc.drag_items_for_choice(choiceno).size(); i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (dragitem) {
|
||||
dragitem.removeClass('item' + i);
|
||||
if (!dragitem.hasClass('beingdragged')) {
|
||||
bgimgxy = this.convert_to_bg_img_xy(dragitem.getXY());
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
dragitem.removeClass('item' + i);
|
||||
dragitem.addClass('item' + coords.length);
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dropped !== null){
|
||||
bgimgxy = this.convert_to_bg_img_xy(dropped.getXY());
|
||||
dropped.addClass('item' + coords.length);
|
||||
if (this.xy_in_bgimg(bgimgxy)) {
|
||||
coords[coords.length] = bgimgxy;
|
||||
}
|
||||
}
|
||||
this.set_form_value(choiceno, coords.join(';'));
|
||||
},
|
||||
reset_drag_xy : function (choiceno) {
|
||||
this.set_form_value(choiceno, '');
|
||||
},
|
||||
set_form_value : function (choiceno, value) {
|
||||
this.doc.input_for_choice(choiceno).set('value', value);
|
||||
},
|
||||
//make sure xy value is not out of bounds of bg image
|
||||
xy_in_bgimg : function (bgimgxy) {
|
||||
if ((bgimgxy[0] < 0) ||
|
||||
(bgimgxy[1] < 0) ||
|
||||
(bgimgxy[0] > this.doc.bg_img().get('width')) ||
|
||||
(bgimgxy[1] > this.doc.bg_img().get('height'))){
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
constrain_to_bgimg : function (windowxy) {
|
||||
var bgimgxy = this.convert_to_bg_img_xy(windowxy);
|
||||
bgimgxy[0] = Math.max(0, bgimgxy[0]);
|
||||
bgimgxy[1] = Math.max(0, bgimgxy[1]);
|
||||
bgimgxy[0] = Math.min(this.doc.bg_img().get('width'), bgimgxy[0]);
|
||||
bgimgxy[1] = Math.min(this.doc.bg_img().get('height'), bgimgxy[1]);
|
||||
return this.convert_to_window_xy(bgimgxy);
|
||||
},
|
||||
convert_to_bg_img_xy : function (windowxy) {
|
||||
return [Number(windowxy[0]) - this.doc.bg_img().getX() - 1,
|
||||
Number(windowxy[1]) - this.doc.bg_img().getY() - 1];
|
||||
},
|
||||
redraw_drags_and_drops : function() {
|
||||
this.doc.drag_items().each(function(item) {
|
||||
//if (!item.hasClass('beingdragged')){
|
||||
item.addClass('unneeded');
|
||||
//}
|
||||
}, this);
|
||||
this.doc.inputs_for_choices().each(function (input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var coords = this.get_coords(input);
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var dragitem = this.doc.drag_item_for_choice(choiceno, i);
|
||||
if (!dragitem || dragitem.hasClass('beingdragged')) {
|
||||
dragitem = this.clone_new_drag_item(dragitemhome, i);
|
||||
} else {
|
||||
dragitem.removeClass('unneeded');
|
||||
}
|
||||
dragitem.setXY(coords[i]);
|
||||
}
|
||||
}, this);
|
||||
this.doc.drag_items().each(function(item) {
|
||||
if (item.hasClass('unneeded') && !item.hasClass('beingdragged')) {
|
||||
item.remove(true);
|
||||
}
|
||||
}, this);
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.clear();
|
||||
} else {
|
||||
this.graphics = new Y.Graphic(
|
||||
{render:this.doc.top_node().one("div.ddarea div.dropzones")}
|
||||
);
|
||||
}
|
||||
if (this.get('dropzones').length !== 0) {
|
||||
this.restart_colours();
|
||||
for (var dropzoneno in this.get('dropzones')) {
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
var d = this.get('dropzones')[dropzoneno];
|
||||
this.draw_drop_zone(dropzoneno, d.markertext,
|
||||
d.shape, d.coords, colourfordropzone, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine what drag items need to be shown and
|
||||
* return coords of all drag items except any that are currently being dragged
|
||||
* based on contents of hidden inputs and whether drags are 'infinite' or how many drags should be shown.
|
||||
*/
|
||||
get_coords : function (input) {
|
||||
var choiceno = this.get_choiceno_for_node(input);
|
||||
var fv = input.get('value');
|
||||
var infinite = input.hasClass('infinite');
|
||||
var noofdrags = this.get_noofdrags_for_node(input);
|
||||
var dragging = (null !== this.doc.drag_item_being_dragged(choiceno));
|
||||
var coords = [];
|
||||
if (fv !== '') {
|
||||
var coordsstrings = fv.split(';');
|
||||
for (var i = 0; i < coordsstrings.length; i++) {
|
||||
coords[coords.length] = this.convert_to_window_xy(coordsstrings[i].split(','));
|
||||
}
|
||||
}
|
||||
var displayeddrags = coords.length + (dragging ? 1 : 0);
|
||||
if (infinite || (displayeddrags < noofdrags)) {
|
||||
coords[coords.length] = this.drag_home_xy(choiceno);
|
||||
}
|
||||
return coords;
|
||||
},
|
||||
drag_home_xy : function (choiceno) {
|
||||
var dragitemhome = this.doc.drag_item_home(choiceno);
|
||||
return [dragitemhome.getX(), dragitemhome.getY() - 12];
|
||||
},
|
||||
get_choiceno_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'choice'));
|
||||
},
|
||||
get_itemno_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'item'));
|
||||
},
|
||||
get_noofdrags_for_node : function(node) {
|
||||
return Number(this.doc.get_classname_numeric_suffix(node, 'noofdrags'));
|
||||
},
|
||||
|
||||
// Keyboard accessibility stuff below here.
|
||||
drop_zone_key_press : function (e) {
|
||||
var dragitem = e.target;
|
||||
var xy = dragitem.getXY();
|
||||
switch (e.direction) {
|
||||
case 'left' :
|
||||
xy[0] -= 1;
|
||||
break;
|
||||
case 'right' :
|
||||
xy[0] += 1;
|
||||
break;
|
||||
case 'down' :
|
||||
xy[1] += 1;
|
||||
break;
|
||||
case 'up' :
|
||||
xy[1] -= 1;
|
||||
break;
|
||||
case 'remove' :
|
||||
xy = null;
|
||||
break;
|
||||
}
|
||||
var choiceno = this.get_choiceno_for_node(dragitem);
|
||||
if (xy !== null) {
|
||||
xy = this.constrain_to_bgimg(xy);
|
||||
} else {
|
||||
xy = this.drag_home_xy(choiceno);
|
||||
}
|
||||
e.preventDefault();
|
||||
dragitem.setXY(xy);
|
||||
this.save_all_xy_for_choice(choiceno, null);
|
||||
}
|
||||
}, {NAME : DDMARKERQUESTIONNAME, ATTRS : {dropzones:{value:[]}}});
|
||||
|
||||
Y.Event.define('dragchange', {
|
||||
// Webkit and IE repeat keydown when you hold down arrow keys.
|
||||
// Opera links keypress to page scroll; others keydown.
|
||||
// Firefox prevents page scroll via preventDefault() on either
|
||||
// keydown or keypress.
|
||||
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
|
||||
|
||||
_keys: {
|
||||
'32': 'remove', // Space
|
||||
'37': 'left', // Left arrow
|
||||
'38': 'up', // Up arrow
|
||||
'39': 'right', // Right arrow
|
||||
'40': 'down', // Down arrow
|
||||
'65': 'left', // a
|
||||
'87': 'up', // w
|
||||
'68': 'right', // d
|
||||
'83': 'down', // s
|
||||
'27': 'remove' // Escape
|
||||
},
|
||||
|
||||
_keyHandler: function (e, notifier) {
|
||||
if (this._keys[e.keyCode]) {
|
||||
e.direction = this._keys[e.keyCode];
|
||||
notifier.fire(e);
|
||||
}
|
||||
},
|
||||
|
||||
on: function (node, sub, notifier) {
|
||||
sub._detacher = node.on(this._event, this._keyHandler,
|
||||
this, notifier);
|
||||
}
|
||||
});
|
||||
M.qtype_ddmarker.init_question = function(config) {
|
||||
return new DDMARKER_QUESTION(config);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"moodle-qtype_ddmarker-dd": {
|
||||
"requires": [
|
||||
"node",
|
||||
"event-resize",
|
||||
"dd",
|
||||
"dd-drop",
|
||||
"dd-constrain",
|
||||
"graphics"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "moodle-qtype_ddmarker-form",
|
||||
"builds": {
|
||||
"moodle-qtype_ddmarker-form": {
|
||||
"jsfiles": [
|
||||
"form.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This is the question editing form code.
|
||||
*/
|
||||
var DDMARKERFORMNAME = 'moodle-qtype_ddmarker-form';
|
||||
var DDMARKER_FORM = function() {
|
||||
DDMARKER_FORM.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
|
||||
fp : null,
|
||||
|
||||
initializer : function() {
|
||||
var pendingid = 'qtype_ddmarker-form-' + Math.random().toString(36).slice(2); // Random string.
|
||||
M.util.js_pending(pendingid);
|
||||
this.fp = this.file_pickers();
|
||||
var tn = Y.one(this.get('topnode'));
|
||||
tn.one('div.fcontainer').append(
|
||||
'<div class="ddarea">' +
|
||||
'<div class="markertexts"></div>' +
|
||||
'<div class="droparea"></div>' +
|
||||
'<div class="dropzones"></div>' +
|
||||
'<div class="grid"></div>' +
|
||||
'</div>');
|
||||
this.doc = this.doc_structure(this);
|
||||
this.stop_selector_events();
|
||||
this.set_options_for_drag_item_selectors();
|
||||
this.setup_form_events();
|
||||
Y.later(500, this, this.update_drop_zones, [pendingid], true);
|
||||
Y.after(this.load_bg_image, M.form_filepicker, 'callback', this);
|
||||
this.load_bg_image();
|
||||
},
|
||||
|
||||
load_bg_image : function() {
|
||||
var bgimageurl = this.fp.file('bgimage').href;
|
||||
if (bgimageurl !== null) {
|
||||
this.doc.load_bg_img(bgimageurl);
|
||||
|
||||
var drop = new Y.DD.Drop({
|
||||
node: this.doc.bg_img()
|
||||
});
|
||||
|
||||
// Listen for a drop:hit on the background image.
|
||||
drop.on('drop:hit', function(e) {
|
||||
e.drag.get('node').setData('gooddrop', true);
|
||||
});
|
||||
|
||||
this.afterimageloaddone = false;
|
||||
this.doc.bg_img().on('load', this.constrain_image_size, this);
|
||||
}
|
||||
},
|
||||
|
||||
constrain_image_size : function (e) {
|
||||
var maxsize = this.get('maxsizes').bgimage;
|
||||
var reduceby = Math.max(e.target.get('width') / maxsize.width,
|
||||
e.target.get('height') / maxsize.height);
|
||||
if (reduceby > 1) {
|
||||
e.target.set('width', Math.floor(e.target.get('width') / reduceby));
|
||||
}
|
||||
e.target.addClass('constrained');
|
||||
e.target.detach('load', this.constrain_image_size);
|
||||
},
|
||||
|
||||
update_drop_zones : function (pendingid) {
|
||||
|
||||
// Set up drop zones.
|
||||
if (this.graphics !== null) {
|
||||
this.graphics.destroy();
|
||||
}
|
||||
this.restart_colours();
|
||||
this.graphics = new Y.Graphic({render:"div.ddarea div.dropzones"});
|
||||
var noofdropzones = this.form.get_form_value('nodropzone', []);
|
||||
for (var dropzoneno = 0; dropzoneno < noofdropzones; dropzoneno++) {
|
||||
var dragitemno = this.form.get_form_value('drops', [dropzoneno, 'choice']);
|
||||
var markertext = this.get_marker_text(dragitemno);
|
||||
var shape = this.form.get_form_value('drops', [dropzoneno, 'shape']);
|
||||
var coords = this.get_coords(dropzoneno);
|
||||
var colourfordropzone = this.get_next_colour();
|
||||
Y.one('input#id_drops_' + dropzoneno + '_coords')
|
||||
.setStyle('background-color', colourfordropzone);
|
||||
this.draw_drop_zone(dropzoneno, markertext,
|
||||
shape, coords, colourfordropzone, false);
|
||||
}
|
||||
if (this.doc.bg_img()) {
|
||||
Y.one('div.ddarea .grid')
|
||||
.setXY(this.doc.bg_img().getXY())
|
||||
.setStyle('width', this.doc.bg_img().get('width'))
|
||||
.setStyle('height', this.doc.bg_img().get('height'));
|
||||
}
|
||||
M.util.js_complete(pendingid);
|
||||
},
|
||||
|
||||
get_coords : function (dropzoneno) {
|
||||
var coords = this.form.get_form_value('drops', [dropzoneno, 'coords']);
|
||||
return coords.replace(new RegExp("\\s*", 'g'), '');
|
||||
},
|
||||
get_marker_text : function (markerno) {
|
||||
if (Number(markerno) !== 0) {
|
||||
var label = this.form.get_form_value('drags', [markerno - 1, 'label']);
|
||||
return label.replace(new RegExp("^\\s*(.*)\\s*$"), "$1");
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set_options_for_drag_item_selectors : function () {
|
||||
var dragitemsoptions = {0: ''};
|
||||
for (var i = 1; i <= this.form.get_form_value('noitems', []); i++) {
|
||||
var label = this.get_marker_text(i);
|
||||
if (label !== "") {
|
||||
dragitemsoptions[i] = Y.Escape.html(label);
|
||||
}
|
||||
}
|
||||
// Get all the currently selected drags for each drop.
|
||||
var selectedvalues = [];
|
||||
var selector;
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
selectedvalues[i] = Number(selector.get('value'));
|
||||
}
|
||||
for (i = 0; i < this.form.get_form_value('nodropzone', []); i++) {
|
||||
selector = Y.one('#id_drops_' + i + '_choice');
|
||||
// Remove all options for drag choice.
|
||||
selector.all('option').remove(true);
|
||||
// And recreate the options.
|
||||
for (var value in dragitemsoptions) {
|
||||
value = Number(value);
|
||||
var option = '<option value="' + value + '">' + dragitemsoptions[value] + '</option>';
|
||||
selector.append(option);
|
||||
var optionnode = selector.one('option[value="' + value + '"]');
|
||||
// Is this the currently selected value?
|
||||
if (value === selectedvalues[i]) {
|
||||
optionnode.set('selected', true);
|
||||
} else {
|
||||
// It is not the currently selected value, is it selectable?
|
||||
if (value !== 0) { // The 'no item' option is always selectable.
|
||||
// Variables to hold form values about this drag item.
|
||||
var noofdrags = this.form.get_form_value('drags', [value - 1, 'noofdrags']);
|
||||
if (Number(noofdrags) !== 0) { // 'noofdrags == 0' means infinite.
|
||||
// Go through all selected values in drop downs.
|
||||
for (var k in selectedvalues) {
|
||||
// Count down 'noofdrags' and if reach zero then set disabled option for this drag item.
|
||||
if (Number(selectedvalues[k]) === value) {
|
||||
if (Number(noofdrags) === 1) {
|
||||
optionnode.set('disabled', true);
|
||||
break;
|
||||
} else {
|
||||
noofdrags--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stop_selector_events : function () {
|
||||
Y.all('fieldset#id_dropzoneheader select').detachAll();
|
||||
},
|
||||
|
||||
setup_form_events : function () {
|
||||
//events triggered by changes to form data
|
||||
|
||||
// Changes to labels.
|
||||
Y.all('fieldset#id_draggableitemheader input').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Changes to selected drag item.
|
||||
Y.all('fieldset#id_draggableitemheader select').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
|
||||
// Change in selected item.
|
||||
Y.all('fieldset#id_dropzoneheader select').on('change', function () {
|
||||
this.set_options_for_drag_item_selectors();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level operations on form.
|
||||
*/
|
||||
form : {
|
||||
to_name_with_index : function(name, indexes) {
|
||||
var indexstring = name;
|
||||
for (var i = 0; i < indexes.length; i++) {
|
||||
indexstring = indexstring + '[' + indexes[i] + ']';
|
||||
}
|
||||
return indexstring;
|
||||
},
|
||||
get_el : function (name, indexes) {
|
||||
var form = document.getElementById('mform1');
|
||||
return form.elements[this.to_name_with_index(name, indexes)];
|
||||
},
|
||||
get_form_value : function(name, indexes) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
return el.checked;
|
||||
} else {
|
||||
return el.value;
|
||||
}
|
||||
},
|
||||
set_form_value : function(name, indexes, value) {
|
||||
var el = this.get_el(name, indexes);
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
},
|
||||
from_name_with_index : function(name) {
|
||||
var toreturn = {};
|
||||
toreturn.indexes = [];
|
||||
var bracket = name.indexOf('[');
|
||||
toreturn.name = name.substring(0, bracket);
|
||||
while (bracket !== -1) {
|
||||
var end = name.indexOf(']', bracket + 1);
|
||||
toreturn.indexes.push(name.substring(bracket + 1, end));
|
||||
bracket = name.indexOf('[', end + 1);
|
||||
}
|
||||
return toreturn;
|
||||
}
|
||||
},
|
||||
|
||||
file_pickers : function () {
|
||||
var draftitemidstoname;
|
||||
var nametoparentnode;
|
||||
if (draftitemidstoname === undefined) {
|
||||
draftitemidstoname = {};
|
||||
nametoparentnode = {};
|
||||
var filepickers = Y.all('form.mform input.filepickerhidden');
|
||||
filepickers.each(function(filepicker) {
|
||||
draftitemidstoname[filepicker.get('value')] = filepicker.get('name');
|
||||
nametoparentnode[filepicker.get('name')] = filepicker.get('parentNode');
|
||||
}, this);
|
||||
}
|
||||
var toreturn = {
|
||||
file : function (name) {
|
||||
var parentnode = nametoparentnode[name];
|
||||
var fileanchor = parentnode.one('div.filepicker-filelist a');
|
||||
if (fileanchor) {
|
||||
return {href : fileanchor.get('href'), name : fileanchor.get('innerHTML')};
|
||||
} else {
|
||||
return {href : null, name : null};
|
||||
}
|
||||
},
|
||||
name : function (draftitemid) {
|
||||
return draftitemidstoname[draftitemid];
|
||||
}
|
||||
};
|
||||
return toreturn;
|
||||
}
|
||||
},{NAME : DDMARKERFORMNAME, ATTRS : {maxsizes:{value:null}}});
|
||||
|
||||
M.qtype_ddmarker = M.qtype_ddmarker || {};
|
||||
M.qtype_ddmarker.init_form = function(config) {
|
||||
return new DDMARKER_FORM(config);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"moodle-qtype_ddmarker-form": {
|
||||
"requires": [
|
||||
"moodle-qtype_ddmarker-dd",
|
||||
"form_filepicker",
|
||||
"graphics",
|
||||
"escape"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Backup code for ddwtos.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Provides the information to backup ddwtos questions.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backup_qtype_ddwtos_plugin extends backup_qtype_plugin {
|
||||
|
||||
/**
|
||||
* Returns the qtype information to attach to question element.
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
|
||||
// Define the virtual plugin element with the condition to fulfill.
|
||||
$plugin = $this->get_plugin_element(null, '../../qtype', 'ddwtos');
|
||||
|
||||
// Create one standard named plugin element (the visible container).
|
||||
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
|
||||
|
||||
// Connect the visible container ASAP.
|
||||
$plugin->add_child($pluginwrapper);
|
||||
|
||||
// This qtype uses standard question_answers, add them here
|
||||
// to the tree before any other information that will use them.
|
||||
$this->add_question_question_answers($pluginwrapper);
|
||||
|
||||
// Now create the qtype own structures.
|
||||
$ddwtos = new backup_nested_element('ddwtos', array('id'), array(
|
||||
'shuffleanswers', 'correctfeedback', 'correctfeedbackformat',
|
||||
'partiallycorrectfeedback', 'partiallycorrectfeedbackformat',
|
||||
'incorrectfeedback', 'incorrectfeedbackformat', 'shownumcorrect'));
|
||||
|
||||
// Now the own qtype tree.
|
||||
$pluginwrapper->add_child($ddwtos);
|
||||
|
||||
// Set source to populate the data.
|
||||
$ddwtos->set_source_table('question_ddwtos', array('questionid' => backup::VAR_PARENTID));
|
||||
|
||||
// Don't need to annotate ids nor files.
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one array with filearea => mappingname elements for the qtype
|
||||
*
|
||||
* Used by {@link get_components_and_fileareas} to know about all the qtype
|
||||
* files to be processed both in backup and restore.
|
||||
*/
|
||||
public static function get_qtype_fileareas() {
|
||||
return array(
|
||||
'correctfeedback' => 'question_created',
|
||||
'partiallycorrectfeedback' => 'question_created',
|
||||
'incorrectfeedback' => 'question_created');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Restore code for qtype_ddwtos.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Restore plugin class that provides the necessary information needed to restore one ddwtos qtype plugin.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_qtype_ddwtos_plugin extends restore_qtype_plugin {
|
||||
|
||||
/**
|
||||
* Returns the paths to be handled by the plugin at question level.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
|
||||
$paths = array();
|
||||
|
||||
// This qtype uses question_answers, add them.
|
||||
$this->add_question_question_answers($paths);
|
||||
|
||||
// Add own qtype stuff.
|
||||
$elename = 'ddwtos';
|
||||
$elepath = $this->get_pathfor('/ddwtos'); // We used get_recommended_name() so this works.
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
return $paths; // And we return the interesting paths.
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qtype/ddwtos element.
|
||||
*
|
||||
* @param array|object $data ddwtos object to work with.
|
||||
*/
|
||||
public function process_ddwtos($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Detect if the question is created or mapped.
|
||||
$oldquestionid = $this->get_old_parentid('question');
|
||||
$newquestionid = $this->get_new_parentid('question');
|
||||
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
|
||||
|
||||
// If the question has been created by restore, we need to create its question_ddwtos too.
|
||||
if ($questioncreated) {
|
||||
// Adjust some columns.
|
||||
$data->questionid = $newquestionid;
|
||||
// Insert record.
|
||||
$newitemid = $DB->insert_record('question_ddwtos', $data);
|
||||
// Create mapping (needed for decoding links).
|
||||
$this->set_mapping('question_ddwtos', $oldid, $newitemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this qtype to be processed by the links decoder.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function define_decode_contents() {
|
||||
|
||||
$contents = array();
|
||||
|
||||
$fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
|
||||
$contents[] = new restore_decode_content('question_ddwtos', $fields, 'question_ddwtos');
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="question/type/ddwtos/db" VERSION="20150914" COMMENT="XMLDB file for Moodle question/type/ddwtos."
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
<TABLES>
|
||||
<TABLE NAME="question_ddwtos" COMMENT="Defines drag and drop (words into sentences) questions">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
|
||||
<FIELD NAME="correctfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any correct response."/>
|
||||
<FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="partiallycorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any partially correct response."/>
|
||||
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="incorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response."/>
|
||||
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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 the editing form for the drag-and-drop words into sentences question type.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/gapselect/edit_form_base.php');
|
||||
|
||||
|
||||
/**
|
||||
* Drag-and-drop words into sentences editing form definition.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddwtos_edit_form extends qtype_gapselect_edit_form_base {
|
||||
public function qtype() {
|
||||
return 'ddwtos';
|
||||
}
|
||||
|
||||
protected function data_preprocessing_choice($question, $answer, $key) {
|
||||
$question = parent::data_preprocessing_choice($question, $answer, $key);
|
||||
$options = unserialize($answer->feedback);
|
||||
$question->choices[$key]['choicegroup'] = $options->draggroup;
|
||||
$question->choices[$key]['infinite'] = $options->infinite;
|
||||
return $question;
|
||||
}
|
||||
|
||||
protected function choice_group($mform) {
|
||||
$grouparray = parent::choice_group($mform);
|
||||
$grouparray[] = $mform->createElement('checkbox', 'infinite', ' ',
|
||||
get_string('infinite', 'qtype_ddwtos'), null,
|
||||
array('size' => 1, 'class' => 'tweakcss'));
|
||||
return $grouparray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Lang file for ddwtos.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['addmorechoiceblanks'] = 'Blanks for {no} more choices';
|
||||
$string['answer'] = 'Answer';
|
||||
$string['correctansweris'] = 'The correct answer is: {$a}';
|
||||
$string['infinite'] = 'Infinite';
|
||||
$string['pleaseputananswerineachbox'] = 'Please put an answer in each box.';
|
||||
$string['pluginname'] = 'Drag and drop into text';
|
||||
$string['pluginname_help'] = 'Type in some question text like "The [[1]] jumped over the [[2]]", then enter the possible words to go in gaps 1 and 2 underneath.';
|
||||
$string['pluginname_link'] = 'question/type/ddwtos';
|
||||
$string['pluginnameadding'] = 'Adding a drag and drop into text';
|
||||
$string['pluginnameediting'] = 'Editing a drag and drop into text';
|
||||
$string['pluginnamesummary'] = 'Missing words in some text are filled in using drag-and-drop.';
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Serve question type files
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2012 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Checks file access for ddwtos questions.
|
||||
*
|
||||
* @param object $course The course we are in
|
||||
* @param object $cm Course module
|
||||
* @param object $context The context object
|
||||
* @param string $filearea the name of the file area.
|
||||
* @param array $args the remaining bits of the file path.
|
||||
* @param bool $forcedownload whether the user must be forced to download the file.
|
||||
* @param array $options additional options affecting the file serving
|
||||
*/
|
||||
function qtype_ddwtos_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
question_pluginfile($course, $context, 'qtype_ddwtos', $filearea, $args, $forcedownload, $options);
|
||||
}
|
||||
|
After Width: | Height: | Size: 373 B |
@@ -0,0 +1,82 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop words into sentences question definition class.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/gapselect/questionbase.php');
|
||||
|
||||
|
||||
/**
|
||||
* Represents a drag-and-drop words into sentences question.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddwtos_question extends qtype_gapselect_question_base {
|
||||
|
||||
public function summarise_choice($choice) {
|
||||
return $this->html_to_text($choice->text, FORMAT_HTML);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents one of the choices (draggable boxes).
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddwtos_choice {
|
||||
/** @var string Text for the choice */
|
||||
public $text;
|
||||
|
||||
/** @var int Group of the choice */
|
||||
public $draggroup;
|
||||
|
||||
/** @var bool If the choice can be used an unlimited number of times */
|
||||
public $infinite;
|
||||
|
||||
/**
|
||||
* Initialize a choice object.
|
||||
*
|
||||
* @param string $text The text of the choice
|
||||
* @param int $draggroup Group of the drop choice
|
||||
* @param bool $infinite True if the item can be used an unlimited number of times
|
||||
*/
|
||||
public function __construct($text, $draggroup = 1, $infinite = false) {
|
||||
$this->text = $text;
|
||||
$this->draggroup = $draggroup;
|
||||
$this->infinite = $infinite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the group of this item.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function choice_group() {
|
||||
return $this->draggroup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Question type class for the drag-and-drop words into sentences question type.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/lib.php');
|
||||
require_once($CFG->dirroot . '/question/format/xml/format.php');
|
||||
require_once($CFG->dirroot . '/question/type/gapselect/questiontypebase.php');
|
||||
|
||||
|
||||
/**
|
||||
* The drag-and-drop words into sentences question type class.
|
||||
*
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddwtos extends qtype_gapselect_base {
|
||||
protected function choice_group_key() {
|
||||
return 'draggroup';
|
||||
}
|
||||
|
||||
protected function choice_options_to_feedback($choice) {
|
||||
$output = new stdClass();
|
||||
$output->draggroup = $choice['choicegroup'];
|
||||
$output->infinite = !empty($choice['infinite']);
|
||||
return serialize($output);
|
||||
}
|
||||
|
||||
protected function feedback_to_choice_options($feedback) {
|
||||
$feedbackobj = unserialize($feedback);
|
||||
return array('draggroup' => $feedbackobj->draggroup, 'infinite' => $feedbackobj->infinite);
|
||||
}
|
||||
|
||||
protected function make_choice($choicedata) {
|
||||
$options = unserialize($choicedata->feedback);
|
||||
return new qtype_ddwtos_choice(
|
||||
$choicedata->answer, $options->draggroup, $options->infinite);
|
||||
}
|
||||
|
||||
public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
|
||||
if (!isset($data['@']['type']) || $data['@']['type'] != 'ddwtos') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$question = $format->import_headers($data);
|
||||
$question->qtype = 'ddwtos';
|
||||
|
||||
$question->shuffleanswers = $format->trans_single(
|
||||
$format->getpath($data, array('#', 'shuffleanswers', 0, '#'), 1));
|
||||
|
||||
if (!empty($data['#']['dragbox'])) {
|
||||
// Modern XML format.
|
||||
$dragboxes = $data['#']['dragbox'];
|
||||
$question->answer = array();
|
||||
$question->draggroup = array();
|
||||
$question->infinite = array();
|
||||
|
||||
foreach ($data['#']['dragbox'] as $dragboxxml) {
|
||||
$question->choices[] = array(
|
||||
'answer' => $format->getpath($dragboxxml, array('#', 'text', 0, '#'), '', true),
|
||||
'choicegroup' => $format->getpath($dragboxxml, array('#', 'group', 0, '#'), 1),
|
||||
'infinite' => array_key_exists('infinite', $dragboxxml['#']),
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Legacy format containing PHP serialisation.
|
||||
foreach ($data['#']['answer'] as $answerxml) {
|
||||
$ans = $format->import_answer($answerxml);
|
||||
$options = unserialize(stripslashes($ans->feedback['text']));
|
||||
$question->choices[] = array(
|
||||
'answer' => $ans->answer,
|
||||
'choicegroup' => $options->draggroup,
|
||||
'infinite' => $options->infinite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$format->import_combined_feedback($question, $data, true);
|
||||
$format->import_hints($question, $data, true, false,
|
||||
$format->get_format($question->questiontextformat));
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
public function export_to_xml($question, qformat_xml $format, $extra = null) {
|
||||
$output = '';
|
||||
|
||||
$output .= ' <shuffleanswers>' . $question->options->shuffleanswers .
|
||||
"</shuffleanswers>\n";
|
||||
|
||||
$output .= $format->write_combined_feedback($question->options,
|
||||
$question->id,
|
||||
$question->contextid);
|
||||
|
||||
foreach ($question->options->answers as $answer) {
|
||||
$options = unserialize($answer->feedback);
|
||||
|
||||
$output .= " <dragbox>\n";
|
||||
$output .= $format->writetext($answer->answer, 3);
|
||||
$output .= " <group>{$options->draggroup}</group>\n";
|
||||
if ($options->infinite) {
|
||||
$output .= " <infinite/>\n";
|
||||
}
|
||||
$output .= " </dragbox>\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Drag-and-drop words into sentences question renderer class.
|
||||
*
|
||||
* @package qtype_ddwtos
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/gapselect/rendererbase.php');
|
||||
|
||||
|
||||
/**
|
||||
* Generates the output for drag-and-drop words into sentences questions.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddwtos_renderer extends qtype_elements_embedded_in_question_text_renderer {
|
||||
|
||||
protected function qtext_classname() {
|
||||
return 'qtext ddwtos_questionid_for_javascript';
|
||||
}
|
||||
|
||||
public function formulation_and_controls(question_attempt $qa,
|
||||
question_display_options $options) {
|
||||
global $PAGE;
|
||||
|
||||
$result = parent::formulation_and_controls($qa, $options);
|
||||
|
||||
$inputids = array();
|
||||
$question = $qa->get_question();
|
||||
foreach ($question->places as $placeno => $place) {
|
||||
$inputids[$placeno] = $this->box_id($qa, $question->field($placeno));
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'inputids' => $inputids,
|
||||
'topnode' => 'div.que.ddwtos#q' . $qa->get_slot(),
|
||||
'readonly' => $options->readonly
|
||||
);
|
||||
|
||||
$PAGE->requires->yui_module('moodle-qtype_ddwtos-dd',
|
||||
'M.qtype_ddwtos.init_question', array($params));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function post_qtext_elements(question_attempt $qa,
|
||||
question_display_options $options) {
|
||||
$result = '';
|
||||
$question = $qa->get_question();
|
||||
|
||||
$dragboxs = '';
|
||||
foreach ($question->choices as $group => $choices) {
|
||||
$dragboxs .= $this->drag_boxes($qa, $group,
|
||||
$question->get_ordered_choices($group), $options);
|
||||
}
|
||||
|
||||
$classes = array('answercontainer');
|
||||
if (!$options->readonly) {
|
||||
$classes[] = 'notreadonly';
|
||||
} else {
|
||||
$classes[] = 'readonly';
|
||||
}
|
||||
$result .= html_writer::tag('div', $dragboxs, array('class' => implode(' ', $classes)));
|
||||
|
||||
$classes = array('drags');
|
||||
if (!$options->readonly) {
|
||||
$classes[] = 'notreadonly';
|
||||
} else {
|
||||
$classes[] = 'readonly';
|
||||
}
|
||||
$result .= html_writer::tag('div', '', array('class' => implode(' ', $classes)));
|
||||
|
||||
// We abuse the clear_wrong method to output the hidden form fields we
|
||||
// want irrespective of whether we are actually clearing the wrong
|
||||
// bits of the response.
|
||||
if (!$options->clearwrong) {
|
||||
$result .= $this->clear_wrong($qa, false);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function embedded_element(question_attempt $qa, $place,
|
||||
question_display_options $options) {
|
||||
$question = $qa->get_question();
|
||||
$group = $question->places[$place];
|
||||
$boxcontents = ' ';
|
||||
|
||||
$value = $qa->get_last_qt_var($question->field($place));
|
||||
|
||||
$attributes = array(
|
||||
'class' => 'place' . $place . ' drop group' . $group
|
||||
);
|
||||
|
||||
if ($options->readonly) {
|
||||
$attributes['class'] .= ' readonly';
|
||||
} else {
|
||||
$attributes['tabindex'] = '0';
|
||||
}
|
||||
|
||||
$feedbackimage = '';
|
||||
if ($options->correctness) {
|
||||
$response = $qa->get_last_qt_data();
|
||||
$fieldname = $question->field($place);
|
||||
if (array_key_exists($fieldname, $response)) {
|
||||
$fraction = (int) ($response[$fieldname] ==
|
||||
$question->get_right_choice_for($place));
|
||||
$feedbackimage = $this->feedback_image($fraction);
|
||||
}
|
||||
}
|
||||
|
||||
return html_writer::tag('span', $boxcontents, $attributes) . ' ' . $feedbackimage;
|
||||
}
|
||||
|
||||
protected function drag_boxes($qa, $group, $choices, question_display_options $options) {
|
||||
$boxes = '';
|
||||
foreach ($choices as $key => $choice) {
|
||||
// Bug 8632: long text entry causes bug in drag and drop field in IE.
|
||||
$content = str_replace('-', '‑', $choice->text);
|
||||
$content = str_replace(' ', ' ', $content);
|
||||
|
||||
$infinite = '';
|
||||
if ($choice->infinite) {
|
||||
$infinite = ' infinite';
|
||||
}
|
||||
|
||||
$boxes .= html_writer::tag('span', $content, array(
|
||||
'class' => 'draghome choice' . $key . ' group' .
|
||||
$choice->draggroup . $infinite)) . ' ';
|
||||
}
|
||||
|
||||
return html_writer::nonempty_tag('div', $boxes,
|
||||
array('class' => 'draggrouphomes' . $choice->draggroup));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually, this question type abuses this method to always ouptut the
|
||||
* hidden fields it needs.
|
||||
*/
|
||||
public function clear_wrong(question_attempt $qa, $reallyclear = true) {
|
||||
$question = $qa->get_question();
|
||||
$response = $qa->get_last_qt_data();
|
||||
|
||||
if (!empty($response) && $reallyclear) {
|
||||
$cleanresponse = $question->clear_wrong_from_response($response);
|
||||
} else {
|
||||
$cleanresponse = $response;
|
||||
}
|
||||
|
||||
$output = '';
|
||||
foreach ($question->places as $place => $group) {
|
||||
$fieldname = $question->field($place);
|
||||
if (array_key_exists($fieldname, $response)) {
|
||||
$value = $response[$fieldname];
|
||||
} else {
|
||||
$value = '0';
|
||||
}
|
||||
if (array_key_exists($fieldname, $cleanresponse)) {
|
||||
$cleanvalue = $cleanresponse[$fieldname];
|
||||
} else {
|
||||
$cleanvalue = '0';
|
||||
}
|
||||
if ($cleanvalue != $value) {
|
||||
$output .= html_writer::empty_tag('input', array(
|
||||
'type' => 'hidden',
|
||||
'id' => $this->box_id($qa, 'p' . $place),
|
||||
'value' => s($value))) .
|
||||
html_writer::empty_tag('input', array(
|
||||
'type' => 'hidden',
|
||||
'name' => $qa->get_qt_field_name($fieldname),
|
||||
'value' => s($cleanvalue)));
|
||||
} else {
|
||||
$output .= html_writer::empty_tag('input', array(
|
||||
'type' => 'hidden',
|
||||
'id' => $this->box_id($qa, 'p' . $place),
|
||||
'name' => $qa->get_qt_field_name($fieldname),
|
||||
'value' => s($value)));
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.que.ddwtos .qtext {
|
||||
margin-bottom: 0.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.que.ddwtos .draghome {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.que.ddwtos .answertext {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.que.ddwtos .drop {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
.que.ddwtos .draghome, .que.ddwtos .drag {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.que.ddwtos .draghome, .que.ddwtos .drag.unplaced{
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
.que.ddwtos .draghome {
|
||||
visibility: hidden;
|
||||
}
|
||||
.que.ddwtos .drag {
|
||||
z-index: 2;
|
||||
}
|
||||
.que.ddwtos .drag.yui3-dd-dragging {
|
||||
z-index: 3;
|
||||
box-shadow: 3px 3px 4px #000;
|
||||
}
|
||||
|
||||
.que.ddwtos .drop.yui3-dd-drop-over.yui3-dd-drop-active-valid {
|
||||
border-color: #0a0;
|
||||
box-shadow: 0 0 5px 5px rgba(255, 255, 150, 1);
|
||||
}
|
||||
|
||||
.que.ddwtos .notreadonly .drag {
|
||||
cursor: move;
|
||||
}
|
||||
.que.ddwtos .readonly .drag {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.que.ddwtos span.incorrect {
|
||||
background-color: #faa;
|
||||
}
|
||||
.que.ddwtos span.correct {
|
||||
background-color: #afa;
|
||||
}
|
||||
|
||||
.que.ddwtos .group1 {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
.que.ddwtos .group2 {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
.que.ddwtos .group3 {
|
||||
background-color: #B0C4DE;
|
||||
}
|
||||
.que.ddwtos .group4 {
|
||||
background-color: #D8BFD8;
|
||||
}
|
||||
.que.ddwtos .group5 {
|
||||
background-color: #87CEFA;
|
||||
}
|
||||
.que.ddwtos .group6 {
|
||||
background-color: #DAA520;
|
||||
}
|
||||
.que.ddwtos .group7 {
|
||||
background-color: #FFD700;
|
||||
}
|
||||
.que.ddwtos .group8 {
|
||||
background-color: #F0E68C;
|
||||
}
|
||||
|
||||
.que.ddwtos sub,
|
||||
.que.ddwtos sup {
|
||||
font-size: 80%;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.que.ddwtos sup {
|
||||
top: -0.4em;
|
||||
}
|
||||
.que.ddwtos sub {
|
||||
bottom: -0.2em;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@qtype @qtype_ddwtos
|
||||
Feature: Test creating a drag and drop into text question
|
||||
As a teacher
|
||||
In order to test my students
|
||||
I need to be able to create drag and drop into text questions
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Course 1"
|
||||
And I navigate to "Question bank" node in "Course administration"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a drag and drop into text question
|
||||
When I add a "Drag and drop into text" question filling the form with:
|
||||
| Question name | Drag and drop into text 001 |
|
||||
| Question text | The [[1]] [[2]] on the [[3]]. |
|
||||
| General feedback | The cat sat on the mat. |
|
||||
| id_choices_0_answer | cat |
|
||||
| id_choices_1_answer | sat |
|
||||
| id_choices_2_answer | mat |
|
||||
| id_choices_3_answer | dog |
|
||||
| id_choices_4_answer | table |
|
||||
| Hint 1 | First hint |
|
||||
| Hint 2 | Second hint |
|
||||
Then I should see "Drag and drop into text 001"
|
||||