Merge branch 'MDL-47494-ddmarker' into MDL-47494
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
Drag-and-drop markers question type
|
||||
-----------------------------------
|
||||
|
||||
You can use markers with text labels as drag items onto rectangular, circular or
|
||||
polygon drop zones on a background image.
|
||||
|
||||
This question type requires that gapselect question type
|
||||
https://github.com/moodleou/moodle-qtype_ddimageortext/ and
|
||||
https://github.com/moodleou/moodle-qtype_gapselect/
|
||||
to be installed in order to work.
|
||||
|
||||
This question type was written by Jamie Pratt (http://jamiep.org/) for the Open
|
||||
University.
|
||||
|
||||
This version of this question type is compatible with Moodle 2.5+. There are
|
||||
other versions available for Moodle 2.3+.
|
||||
|
||||
###Installation
|
||||
|
||||
####Installation Using Git
|
||||
|
||||
To install using git for a 2.3+ Moodle installation, type this command in the
|
||||
root of your Moodle install:
|
||||
git clone git://github.com/moodleou/moodle-qtype_ddmarker.git question/type/ddmarker
|
||||
Then add question/type/ddmarker to your git ignore.
|
||||
|
||||
####Installation From Downloaded zip file
|
||||
|
||||
Alternatively, download the zip from:
|
||||
https://github.com/moodleou/moodle-qtype_ddmarker/zipball/master
|
||||
unzip it into the question/type folder, and then rename the new folder to ddmarker.
|
||||
|
||||
###Converting 'image target' type questions to this type
|
||||
|
||||
The imagetarget question type question type will not be upgraded to use with Moodle beyond version 1.9 it seems.
|
||||
|
||||
But you can convert your existing image target questions and question attempt data to be drag and drop marker questions and they
|
||||
will work as the image target previously worked.
|
||||
|
||||
There are two ways to convert your imagetarget questions to ddmarker question types.
|
||||
|
||||
####Automatic Conversion (recommended)
|
||||
|
||||
Conversion will happen automatically when you upgrade for Moodle 2.0.
|
||||
|
||||
It is recommended you follow the following steps:
|
||||
|
||||
Upgrade your site to Moodle 2.0. This involves:
|
||||
|
||||
* Upgrade the code base to Moodle 2.0
|
||||
* Remove the imagetarget question type code from question/type/imagetarget/
|
||||
* Go to your http://{moodleroot}/admin/ to trigger the upgrade of the db
|
||||
|
||||
At this point your imagetarget questions won't work and will show up as
|
||||
'missing type' but once that upgrade is done change your Moodle code to Moodle 2.2+
|
||||
|
||||
* install the ddmarker question type code in question type ddmarker.
|
||||
* then go to your http://{moodleroot}/admin/ to trigger the upgrade of the db
|
||||
|
||||
This will convert all your imagetarget questions to ddmarker automatically.
|
||||
|
||||
####Manual Conversion
|
||||
|
||||
You can also use a manual script to convert imagetarget questions to ddmarker
|
||||
after upgrading to Moodle 2.1 or greater.
|
||||
|
||||
* Log in as admin.
|
||||
* You will find a script in the admin menu under plugins/question types/ to
|
||||
convert your imagetarget questions to ddmarker.
|
||||
(This just converts your questions themselves.)
|
||||
* In order to convert your question attempt data to be used with the ddmarker
|
||||
question type and Moodle 2.1 or greater you need to find the 'question engine
|
||||
upgrade helper' which will appear at the root of the admin menu. Use this script to:
|
||||
* _Reset the upgrade of all attempt data._
|
||||
* _Run the attempt data upgrade again._
|
||||
|
||||
###Issues with converting image target question type questions
|
||||
|
||||
####Background image shrinkage
|
||||
|
||||
The ddmarker question type will shrink the background image of your questions to
|
||||
be within a maximum size and width while at the time preserving aspect ratio of
|
||||
the image. The default max width is 600 pixels and height 400 pixels.
|
||||
|
||||
The ddmarker question type does this whenever you edit a question and save it again.
|
||||
|
||||
Unfortunately when you open up a question to edit it this means that if the
|
||||
image is to big it will be shrunk but at present the position of your drop zones
|
||||
are not moved to compensate for this shrinkage. There won't be a problem until
|
||||
you try to edit and save a question but when you open it in the editor the drop
|
||||
zones will not be in the correct position. If you save the question they
|
||||
they will then be saved in the wrong position.
|
||||
|
||||
####Work around for background image shrinkage
|
||||
|
||||
Either:
|
||||
|
||||
* do not edit questions which have sizes above the allowed max.
|
||||
* or you can change the allowed maximum size of images there are two constant at
|
||||
the top of question/type/ddmarker/questiontype.php that define the size limits.
|
||||
|
||||
The limit is just there to shrink outrageously large images down to a reasonable
|
||||
size automatically. You could set the value of the constants defining the max
|
||||
size to a very large value to effectively disable the image size constraints all
|
||||
together.
|
||||
|
||||
####Lack of drag label in imagetarget questions
|
||||
|
||||
Normally each draggable marker in the ddmarker question type has a text label.
|
||||
There are no labels for the imagetarget markers though, I needed to pick
|
||||
something that will work in any language as the label for the single imagetarget
|
||||
draggable marker so I picked 'X'.
|
||||
|
||||
####Work around for lack of drag label in imagetarget questions
|
||||
|
||||
Your teachers can edit this label after the question has been created. And they
|
||||
might also like to take advantage of the ability in the ddmarker question type
|
||||
to be able to specify the correct drop zones for more than one drag marker onto
|
||||
the same image, giving each drag marker a different label.
|
||||
|
||||
You can change the default drag marker label which you can find defined in a
|
||||
constant at the top of question/type/ddmarker/lib.php
|
||||
|
||||
####No automatic feedback telling the user whether they got the question right, partially right or wrong.
|
||||
|
||||
In the imagetarget question you get a text message telling the user whether they
|
||||
got the question correct, partially correct or wrong. In many of the new
|
||||
question types in Moodle 2.1 onwards the teacher is expected to enter the
|
||||
message that the user will see in the question definition. This is called
|
||||
'combined feedback' and by default it is blank. So questions that have been
|
||||
converted from per 2.1 Moodle, from imagetarget question types that used to
|
||||
give some feedback to the user will no longer do so.
|
||||
|
||||
####Work around for lack of automatic feedback telling the user whether they got
|
||||
|
||||
the question right, partially right or wrong.
|
||||
|
||||
You can use this plug in
|
||||
https://github.com/jamiepratt/moodle-admin_tool_questionaddfeedback to bulk add
|
||||
feedback to questions.
|
||||
@@ -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,157 @@
|
||||
<?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 {
|
||||
|
||||
protected static function qtype_name() {
|
||||
return 'ddmarker';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths to be handled by the plugin at question level.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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/drag element.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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,91 @@
|
||||
<?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 installation code.
|
||||
*
|
||||
* @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();
|
||||
|
||||
|
||||
/**
|
||||
* Installation code for the ddmarker question type.
|
||||
*
|
||||
* It converts all existing imagetarget questions to ddmarker
|
||||
*/
|
||||
function xmldb_qtype_ddmarker_install() {
|
||||
global $DB, $OUTPUT;
|
||||
|
||||
$from = 'FROM {question_categories} cat, {question} q';
|
||||
$where = ' WHERE q.qtype = \'imagetarget\' AND q.category = cat.id ';
|
||||
|
||||
$sql = 'SELECT q.*, cat.contextid '.$from.$where.'ORDER BY cat.id, q.name';
|
||||
|
||||
$questions = $DB->get_records_sql($sql);
|
||||
|
||||
if (!empty($questions)) {
|
||||
require_once(dirname(__FILE__).'/../lib.php');
|
||||
$dragssql = 'SELECT drag.* '.$from.', {qtype_ddmarker_drags} drag'.$where.' AND drag.questionid = q.id';
|
||||
$drags = qtype_ddmarker_index_array_of_records_by_key('questionid', $DB->get_records_sql($dragssql));
|
||||
|
||||
$dropssql = 'SELECT drp.* '.$from.', {qtype_ddmarker_drops} drp'.$where.' AND drp.questionid = q.id';
|
||||
$drops = qtype_ddmarker_index_array_of_records_by_key('questionid', $DB->get_records_sql($dropssql));
|
||||
|
||||
$answerssql = 'SELECT answer.* '.$from.', {question_answers} answer'.$where.' AND answer.question = q.id';
|
||||
$answers = qtype_ddmarker_index_array_of_records_by_key('question', $DB->get_records_sql($answerssql));
|
||||
|
||||
$imgfiles = $DB->get_records_sql_menu('SELECT question, qimage FROM {question_imagetarget}');
|
||||
$progressbar = new progress_bar('qtype_ddmarker_convert_from_imagetarget');
|
||||
$progressbar->create();
|
||||
$done = 0;
|
||||
foreach ($questions as $question) {
|
||||
qtype_ddmarker_convert_image_target_question($question, $imgfiles[$question->id], $answers[$question->id]);
|
||||
$done++;
|
||||
$progressbar->update($done, count($questions),
|
||||
get_string('convertingimagetargetquestion', 'qtype_ddmarker', $question));
|
||||
}
|
||||
list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($questions));
|
||||
$DB->delete_records_select('question_answers', 'question '.$qsql, $qparams);
|
||||
$dbman = $DB->get_manager();
|
||||
$dbman->drop_table(new xmldb_table('question_imagetarget'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper used by {@link xmldb_qtype_ddmarker_install}.
|
||||
*
|
||||
* Convert a one-dimensional array of records into a two-dimensional array
|
||||
* grouped by field $key.
|
||||
*
|
||||
* @param string $key the key to group by.
|
||||
* @param array $recs The records to group.
|
||||
* @return array the re-grouped array.
|
||||
*/
|
||||
function qtype_ddmarker_index_array_of_records_by_key($key, $recs) {
|
||||
$out = array();
|
||||
foreach ($recs as $id => $rec) {
|
||||
if (!isset($out[$rec->{$key}])) {
|
||||
$out[$rec->{$key}] = array();
|
||||
}
|
||||
$out[$rec->{$key}][$id] = $rec;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="question/type/ddmarker/db" VERSION="20130530" 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,63 @@
|
||||
<?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,85 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Upgrade library code for the ddmarker question type. This will only get triggered by the code
|
||||
* to convert imagetarget questions to ddmarker.
|
||||
*
|
||||
* @package qtype
|
||||
* @subpackage 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();
|
||||
|
||||
/**
|
||||
* Class for converting attempt data from imagetarget questions when converting
|
||||
* attempts to the new question engine.
|
||||
*
|
||||
* This class is used by the code in question/engine/upgrade/upgradelib.php.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_qe2_attempt_updater extends question_qtype_attempt_updater {
|
||||
public function right_answer() {
|
||||
$drag = reset($this->question->options->drags);
|
||||
return '{'.get_string('dropzone', 'qtype_ddmarker', '1')." -> ".$drag->label.'}';
|
||||
}
|
||||
|
||||
public function was_answered($state) {
|
||||
return !empty($state->answer);
|
||||
}
|
||||
|
||||
public function response_summary($state) {
|
||||
if (!empty($state->answer)) {
|
||||
$drag = reset($this->question->options->drags);
|
||||
foreach ($this->question->options->drops as $drop) {
|
||||
list($xy, $wh) = explode(';', $drop->coords);
|
||||
list($x, $y) = explode(',', $xy);
|
||||
list($w, $h) = explode(',', $wh);
|
||||
list($answerx, $answery) = explode(',', $state->answer);
|
||||
if (($answerx >= $x && $answerx <= ($x + $w)) && ($answery >= $y && $answery <= ($y + $h))) {
|
||||
return '{'.get_string('dropzone', 'qtype_ddmarker', $drop->no)." -> ".$drag->label.'}';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function question_summary() {
|
||||
$drag = reset($this->question->options->drags);
|
||||
return parent::question_summary().'[['.get_string('dropzone', 'qtype_ddmarker', '1')."]] -> {".$drag->label.'}';
|
||||
}
|
||||
|
||||
public function set_first_step_data_elements($state, &$data) {
|
||||
$data['_choiceorder1'] = '1';
|
||||
}
|
||||
|
||||
public function supply_missing_first_step_data(&$data) {
|
||||
}
|
||||
|
||||
public function set_data_elements_for_step($state, &$data) {
|
||||
if (!empty($state->answer)) {
|
||||
$data['c1'] = $state->answer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?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.
|
||||
* @return object $question 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;
|
||||
}
|
||||
|
||||
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,174 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This page lets admin convert imagetarget questions to the ddmarker 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
|
||||
*/
|
||||
|
||||
|
||||
require_once(dirname(__FILE__) . '/../../../config.php');
|
||||
require_once($CFG->libdir . '/adminlib.php');
|
||||
require_once(dirname(__FILE__).'/questionlists.php');
|
||||
require_once(dirname(__FILE__).'/lib.php');
|
||||
|
||||
|
||||
/**
|
||||
* Class representing a list of questions.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_question_converter_list extends qtype_ddmarker_question_list {
|
||||
protected function new_list_item($stringidentifier, $link, $record) {
|
||||
return new qtype_ddmarker_question_converter_list_item($stringidentifier, $link, $record, $this, $this->categorylist);
|
||||
}
|
||||
public function prepare_for_processing($top) {
|
||||
global $DB;
|
||||
$questionids = $top->question_ids();
|
||||
list($inorequalsql, $inorequalparams) = $DB->get_in_or_equal($questionids);
|
||||
$imagetargetrecords = $DB->get_records_select('question_imagetarget', 'question '.$inorequalsql, $inorequalparams);
|
||||
$answers = array();
|
||||
foreach ($imagetargetrecords as $imagetargetrecord) {
|
||||
$this->get_instance($imagetargetrecord->question)->imagetargetrecord = $imagetargetrecord;
|
||||
}
|
||||
$answerrecords = $DB->get_records_select('question_answers', 'question '.$inorequalsql, $inorequalparams);
|
||||
foreach ($answerrecords as $answerrecord) {
|
||||
$this->get_instance($answerrecord->question)->answers[] = $answerrecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class representing an item in the list of questions.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_ddmarker_question_converter_list_item extends qtype_ddmarker_question_list_item {
|
||||
public $imagetargetrecord = null;
|
||||
public $answers = array();
|
||||
public function process($renderer) {
|
||||
qtype_ddmarker_convert_image_target_question($this->record, $this->imagetargetrecord->qimage, $this->answers);
|
||||
parent::process($renderer); // Outputs progress message.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$categoryid = optional_param('categoryid', 0, PARAM_INT);
|
||||
$qcontextid = optional_param('contextid', 0, PARAM_INT);
|
||||
$questionid = optional_param('questionid', 0, PARAM_INT);
|
||||
$confirm = optional_param('confirm', 0, PARAM_INT);
|
||||
|
||||
// Check the user is logged in.
|
||||
require_login();
|
||||
$context = context_system::instance();
|
||||
require_capability('moodle/question:config', $context);
|
||||
|
||||
admin_externalpage_setup('qtypeddmarkerfromimagetarget');
|
||||
|
||||
// Header.
|
||||
$renderer = $PAGE->get_renderer('qtype_ddmarker', 'list');
|
||||
echo $renderer->header();
|
||||
echo $renderer->heading(get_string('imagetargetconverter', 'qtype_ddmarker'), 2);
|
||||
|
||||
|
||||
$params = array();
|
||||
$from = 'FROM {question_categories} cat, {question} q';
|
||||
$where = ' WHERE q.qtype = \'imagetarget\' AND q.category = cat.id ';
|
||||
|
||||
if ($qcontextid) {
|
||||
$qcontext = context::instance_by_id($qcontextid);
|
||||
$from .= ', {context} context';
|
||||
$where .= 'AND cat.contextid = context.id AND (context.path LIKE :path OR context.id = :id) ';
|
||||
$params['path'] = $qcontext->path.'/%';
|
||||
$params['id'] = $qcontext->id;
|
||||
} else if ($categoryid) {
|
||||
// Fetch all questions from this cats context.
|
||||
$from .= ', {question_categories} cat2';
|
||||
$where .= 'AND cat.contextid = cat2.contextid AND cat2.id = :categoryid ';
|
||||
$params['categoryid'] = $categoryid;
|
||||
} else if ($questionid) {
|
||||
// Fetch all questions from this cats context.
|
||||
$where .= 'AND q.id = :questionid ';
|
||||
$params['questionid'] = $questionid;
|
||||
}
|
||||
$sql = 'SELECT q.*, cat.contextid '.$from.$where.'ORDER BY cat.id, q.name';
|
||||
|
||||
$questions = $DB->get_records_sql($sql, $params);
|
||||
|
||||
if (!count($questions)) {
|
||||
echo html_writer::tag('div', get_string('noquestionsfound', 'qtype_ddmarker'));
|
||||
} else {
|
||||
$contextids = array();
|
||||
foreach ($questions as $question) {
|
||||
$contextids[] = $question->contextid;
|
||||
}
|
||||
|
||||
$questionsselected = (bool) ($categoryid || $qcontextid || $questionid);
|
||||
if (!$confirm) {
|
||||
if (!$questionsselected) {
|
||||
$pagestate = 'listall';
|
||||
} else {
|
||||
$pagestate = 'confirm';
|
||||
}
|
||||
} else if (confirm_sesskey()) {
|
||||
$pagestate = 'processing';
|
||||
}
|
||||
$link = ($pagestate == 'listall');
|
||||
$contextlist = new qtype_ddmarker_context_list($pagestate, $link, array_unique($contextids));
|
||||
$categorylist = new qtype_ddmarker_category_list($pagestate, $link, $contextids, $contextlist);
|
||||
$questionlist = new qtype_ddmarker_question_converter_list($pagestate, $link, $questions, $categorylist);
|
||||
|
||||
foreach ($questions as $question) {
|
||||
$questionlist->leaf_node($question->id, 1);
|
||||
}
|
||||
if ($questionid) {
|
||||
$top = $questionlist->get_instance($questionid);
|
||||
} else if ($categoryid) {
|
||||
$top = $categorylist->get_instance($categoryid);
|
||||
} else if ($qcontextid) {
|
||||
$top = $contextlist->get_instance($qcontextid);
|
||||
} else {
|
||||
$top = $contextlist->root_node();
|
||||
}
|
||||
switch ($pagestate) {
|
||||
case 'listall' :
|
||||
echo $renderer->render_qtype_ddmarker_list($top);
|
||||
break;
|
||||
case 'confirm' :
|
||||
echo $renderer->render_qtype_ddmarker_list($top);
|
||||
$cofirmedurl = new moodle_url($PAGE->url, compact('categoryid', 'contextid', 'questionid') + array('confirm' => 1));
|
||||
$cancelurl = new moodle_url($PAGE->url);
|
||||
echo $renderer->confirm(get_string('confirmimagetargetconversion', 'qtype_ddmarker'), $cofirmedurl, $cancelurl);
|
||||
break;
|
||||
case 'processing' :
|
||||
$questionlist->prepare_for_processing($top);
|
||||
echo '<ul>';
|
||||
$top->process($renderer);
|
||||
echo '</ul>';
|
||||
break;
|
||||
default :
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Footer.
|
||||
echo $renderer->footer();
|
||||
@@ -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/>.
|
||||
|
||||
/**
|
||||
* 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['confirmimagetargetconversion'] = 'You are about to convert the above image target questions to the drag and drop markers question type.';
|
||||
$string['coords'] = 'Coords';
|
||||
$string['convertingimagetargetquestion'] = 'Converted question "{$a->name}"';
|
||||
$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['imagetargetconverter'] = 'Convert image target questions to drag and drop marker';
|
||||
$string['infinite'] = 'Infinite';
|
||||
$string['listitemconfirmcategory'] = 'About to convert all imagetarget questions in category "{$a->name}" (contains {$a->qcount} imagetarget questions)';
|
||||
$string['listitemconfirmcontext'] = 'About to convert all imagetarget questions in context "{$a->name}" (contains {$a->qcount} imagetarget questions)';
|
||||
$string['listitemconfirmquestion'] = 'About to convert question "{$a->name}"';
|
||||
$string['listitemlistallcategory'] = 'Select all imagetarget questions in category "{$a->name}" (contains {$a->qcount} imagetarget questions)';
|
||||
$string['listitemlistallcontext'] = 'Select all imagetarget questions in context "{$a->name}" (contains {$a->qcount} imagetarget questions)';
|
||||
$string['listitemlistallquestion'] = 'Select question "{$a->name}"';
|
||||
$string['listitemprocessingcategory'] = 'Converting all imagetarget questions in category "{$a->name}" (contains {$a->qcount} imagetarget questions)';
|
||||
$string['listitemprocessingcontext'] = 'Converting all imagetarget questions in context "{$a->name}" (contains {$a->qcount} imagetarget questions)';
|
||||
$string['listitemprocessingquestion'] = 'Converted question "{$a->name}"';
|
||||
$string['marker'] = 'Marker';
|
||||
$string['marker_n'] = 'Marker {no}';
|
||||
$string['markers'] = 'Markers';
|
||||
$string['nolabel'] = 'No label text';
|
||||
$string['noofdrags'] = 'Number';
|
||||
$string['noquestionsfound'] = 'No questions found to convert here.';
|
||||
$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,137 @@
|
||||
<?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();
|
||||
|
||||
/**
|
||||
* @var string label to use for drag items when converting image target questions to ddmarker question type
|
||||
*/
|
||||
define('QTYPE_DDMARKER_LABEL_FOR_MARKER_FOR_IMAGE_TARGET_QS', 'X');
|
||||
|
||||
/**
|
||||
* Checks file access for essay questions.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
function qtype_ddmarker_course_context_id($catcontextid) {
|
||||
$context = context::instance_by_id($catcontextid);
|
||||
while ($context->contextlevel != CONTEXT_COURSE) {
|
||||
$context = $context->get_parent_context();
|
||||
}
|
||||
return $context->id;
|
||||
}
|
||||
|
||||
function qtype_ddmarker_convert_image_target_question($question, $imgfilename, $answers) {
|
||||
global $DB, $OUTPUT;
|
||||
$correctfeedback = '';
|
||||
$correctfeedbackformat = 1;
|
||||
$incorrectfeedback = '';
|
||||
$incorrectfeedbackformat = 1;
|
||||
$foundincorrectanswer = false;
|
||||
foreach ($answers as $answer) {
|
||||
$no = 1;
|
||||
if ('*' !== $answer->answer) {
|
||||
$drop = new stdClass();
|
||||
$drop->questionid = $question->id;
|
||||
$drop->shape = 'rectangle';
|
||||
$drop->no = $no;
|
||||
list($x1, $y1, $x2, $y2) = explode(',', $answer->answer);
|
||||
$width = $x2 - $x1;
|
||||
$height = $y2 - $y1;
|
||||
$drop->coords = "{$x1},{$y1};{$width},{$height}";
|
||||
$drop->choice = 1;
|
||||
$DB->insert_record('qtype_ddmarker_drops', $drop);
|
||||
$no++;
|
||||
$correctfeedback = $answer->feedback;
|
||||
$correctfeedbackformat = $answer->feedbackformat;
|
||||
} else {
|
||||
$foundincorrectanswer = true;
|
||||
$incorrectfeedback = $answer->feedback;
|
||||
$incorrectfeedbackformat = $answer->feedbackformat;
|
||||
}
|
||||
}
|
||||
if (count($answers) < 2) {
|
||||
echo $OUTPUT->notification('There are less than 2 answers. '.
|
||||
'(Normally we expect at least a correct and incorrect answer). '.
|
||||
'For question id '.$question->id.' "'.$question->name.'".',
|
||||
'notifyproblem');
|
||||
}
|
||||
if (!$foundincorrectanswer) {
|
||||
echo $OUTPUT->notification('No incorrect answer found for question id '.$question->id.' "'.$question->name.'".',
|
||||
'notifyproblem');
|
||||
}
|
||||
$drag = new stdClass();
|
||||
$drag->questionid = $question->id;
|
||||
$drag->no = 1;
|
||||
$drag->label = QTYPE_DDMARKER_LABEL_FOR_MARKER_FOR_IMAGE_TARGET_QS;
|
||||
$drag->infinite = 0;
|
||||
$DB->insert_record('qtype_ddmarker_drags', $drag);
|
||||
|
||||
$ddmarker = new stdClass();
|
||||
$ddmarker->questionid = $question->id;
|
||||
$ddmarker->shuffleanswers = 0;
|
||||
$ddmarker->correctfeedback = $correctfeedback;
|
||||
$ddmarker->correctfeedbackformat = $correctfeedbackformat;
|
||||
$ddmarker->partiallycorrectfeedback = '';
|
||||
$ddmarker->partiallycorrectfeedbackformat = 1;
|
||||
$ddmarker->incorrectfeedback = $incorrectfeedback;
|
||||
$ddmarker->incorrectfeedbackformat = $incorrectfeedbackformat;
|
||||
$ddmarker->shownumcorrect = 0;
|
||||
$ddmarker->showmisplaced = 0;
|
||||
$DB->insert_record('qtype_ddmarker', $ddmarker);
|
||||
|
||||
$newrec = clone($question);
|
||||
unset($newrec->contextid);
|
||||
$newrec->qtype = 'ddmarker';
|
||||
$newrec->timemodified = time();
|
||||
$DB->update_record('question', $newrec);
|
||||
|
||||
$fs = get_file_storage();
|
||||
|
||||
// We need to look in the course legacy files area for file.
|
||||
$bgimagefile = $fs->get_file(qtype_ddmarker_course_context_id($question->contextid),
|
||||
'course',
|
||||
'legacy',
|
||||
'0',
|
||||
'/'.dirname($imgfilename).'/',
|
||||
basename($imgfilename));
|
||||
if ($bgimagefile === false) {
|
||||
echo $OUTPUT->notification('File "'.$imgfilename.'" not found in legacy course files area. '.
|
||||
'For question id '.$question->id.' "'.$question->name.'".',
|
||||
'notifyproblem');
|
||||
} else {
|
||||
$newbgimagefile = new stdClass();
|
||||
$newbgimagefile->component = 'qtype_ddmarker';
|
||||
$newbgimagefile->filearea = 'bgimage';
|
||||
$newbgimagefile->filepath = '/';
|
||||
$newbgimagefile->itemid = $question->id;
|
||||
$newbgimagefile->contextid = $question->contextid;
|
||||
$fs->create_file_from_storedfile($newbgimagefile, $bgimagefile);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 417 B |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 295 B |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,449 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param int $key 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 {
|
||||
public $text;
|
||||
public $no;
|
||||
public $infinite;
|
||||
public $noofdrags;
|
||||
|
||||
public function __construct($label, $no, $infinite, $noofdrags) {
|
||||
$this->text = $label;
|
||||
$this->infinite = $infinite;
|
||||
$this->no = $no;
|
||||
$this->noofdrags = $noofdrags;
|
||||
}
|
||||
public function choice_group() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
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 {
|
||||
public $group = 1;
|
||||
public $no;
|
||||
public $shape;
|
||||
public $coords;
|
||||
|
||||
public function __construct($no, $shape, $coords) {
|
||||
$this->no = $no;
|
||||
$this->shape = qtype_ddmarker_shape::create($shape, $coords);
|
||||
$this->coords = $coords;
|
||||
}
|
||||
|
||||
public function summarise() {
|
||||
return get_string('summariseplaceno', 'qtype_ddmarker', $this->no);
|
||||
}
|
||||
|
||||
public function drop_hit($xy) {
|
||||
return $this->shape->is_point_in_shape($xy);
|
||||
}
|
||||
|
||||
public function correct_coords() {
|
||||
return $this->shape->center_point();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* These classes handle transforming arrays of records into a linked tree of contexts, categories and questions.
|
||||
*
|
||||
* @package qtype_ddmarker
|
||||
* @copyright 2012 Jamie Pratt
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* These classes handle transforming arrays of records into a linked tree of contexts, categories and questions.
|
||||
*
|
||||
* @copyright 2012 Jamie Pratt
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class qtype_ddmarker_list_item implements renderable {
|
||||
|
||||
/**
|
||||
* @var count of questions contained in this item and sub items.
|
||||
*/
|
||||
protected $qcount = 0;
|
||||
/**
|
||||
* @var children array of pointers to list_items either category_list_items or context_list_items
|
||||
*/
|
||||
protected $children = array();
|
||||
|
||||
protected $stringidentifier;
|
||||
protected $link;
|
||||
protected $record;
|
||||
protected $list;
|
||||
protected $parentlist;
|
||||
protected $listtype = null;
|
||||
|
||||
public function __construct($stringidentifier, $link, $record, $list, $parentlist = null) {
|
||||
$this->stringidentifier = $stringidentifier;
|
||||
$this->link = $link;
|
||||
$this->record = $record;
|
||||
$this->list = $list;
|
||||
$this->parentlist = $parentlist;
|
||||
}
|
||||
|
||||
public function add_child($child) {
|
||||
$this->children[] = $child;
|
||||
// Class array_unique relies on __toString() returning a unique string
|
||||
// to determine if objects in array are the same or not.
|
||||
$this->children = array_unique($this->children);
|
||||
}
|
||||
|
||||
abstract protected function parent_node ();
|
||||
|
||||
|
||||
public function item_name() {
|
||||
return $this->record->name;
|
||||
}
|
||||
|
||||
public function id_param_name() {
|
||||
return $this->listtype.'id';
|
||||
}
|
||||
public function get_id() {
|
||||
return $this->record->id;
|
||||
}
|
||||
public function get_q_count() {
|
||||
return $this->qcount;
|
||||
}
|
||||
public function get_string_identifier() {
|
||||
return $this->stringidentifier;
|
||||
}
|
||||
public function get_linked() {
|
||||
return $this->link;
|
||||
}
|
||||
public function get_list_type() {
|
||||
return $this->listtype;
|
||||
}
|
||||
public function get_children() {
|
||||
return $this->children;
|
||||
}
|
||||
public function leaf_to_root($qcount) {
|
||||
$this->qcount += $qcount;
|
||||
$parent = $this->parent_node();
|
||||
if ($parent !== null) {
|
||||
$parent->add_child($this);
|
||||
$parent->leaf_to_root($qcount);
|
||||
}
|
||||
}
|
||||
|
||||
public function process($renderer) {
|
||||
echo '<li>';
|
||||
echo $renderer->item($this);
|
||||
$this->process_children($renderer);
|
||||
echo '</li>';
|
||||
flush();
|
||||
}
|
||||
|
||||
protected function process_children($renderer) {
|
||||
echo '<ul>';
|
||||
foreach ($this->children as $child) {
|
||||
$child->process($renderer);
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
public function question_ids() {
|
||||
return $this->child_question_ids();
|
||||
}
|
||||
|
||||
protected function child_question_ids() {
|
||||
$ids = array();
|
||||
foreach ($this->children as $child) {
|
||||
$ids = array_merge($ids, $child->question_ids());
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
|
||||
public function __toString() {
|
||||
return get_class($this).' '.$this->record->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the course id in which this item is contained or the id of the front page course
|
||||
*/
|
||||
public function course_context_id() {
|
||||
return $this->parent_node()->course_context_id();
|
||||
}
|
||||
|
||||
}
|
||||
class qtype_ddmarker_category_list_item extends qtype_ddmarker_list_item {
|
||||
protected $listtype = 'category';
|
||||
|
||||
public function parent_node() {
|
||||
if ($this->record->parent == 0) {
|
||||
return $this->parentlist->get_instance($this->record->contextid);
|
||||
} else {
|
||||
return $this->list->get_instance($this->record->parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
class qtype_ddmarker_question_list_item extends qtype_ddmarker_list_item {
|
||||
protected $listtype = 'question';
|
||||
|
||||
public function parent_node() {
|
||||
return $this->parentlist->get_instance($this->record->category);
|
||||
}
|
||||
|
||||
public function question_ids() {
|
||||
return array($this->record->id);
|
||||
}
|
||||
}
|
||||
class qtype_ddmarker_context_list_item extends qtype_ddmarker_list_item {
|
||||
protected $listtype = 'context';
|
||||
public function parent_node() {
|
||||
$pathids = explode('/', $this->record->path);
|
||||
if (count($pathids) >= 3) {
|
||||
return $this->list->get_instance($pathids[count($pathids) - 2]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function item_name() {
|
||||
return context_helper::preload_from_record($this->record)->get_context_name();
|
||||
}
|
||||
|
||||
public function course_context_id() {
|
||||
if ((int)$this->record->contextlevel === CONTEXT_COURSE) {
|
||||
return $this->record->id;
|
||||
} else {
|
||||
return parent::course_context_id();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Describes a nested list of listitems. This class and sub classes contain the functionality to build the nested list.
|
||||
**/
|
||||
abstract class qtype_ddmarker_list {
|
||||
protected $records = array();
|
||||
protected $instances = array();
|
||||
abstract protected function new_list_item($stringidentifier, $link, $record);
|
||||
protected function make_list_item_instances_from_records($stringidentifier, $link) {
|
||||
if (!empty($this->records)) {
|
||||
foreach ($this->records as $id => $record) {
|
||||
$this->instances[$id] = $this->new_list_item($stringidentifier, $link, $record);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function get_instance($id) {
|
||||
return $this->instances[$id];
|
||||
}
|
||||
|
||||
public function leaf_node ($id, $qcount) {
|
||||
$instance = $this->get_instance($id);
|
||||
$instance->leaf_to_root($qcount);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class qtype_ddmarker_context_list extends qtype_ddmarker_list {
|
||||
|
||||
protected function new_list_item($stringidentifier, $link, $record) {
|
||||
return new qtype_ddmarker_context_list_item($stringidentifier, $link, $record, $this);
|
||||
}
|
||||
|
||||
public function __construct($stringidentifier, $link, $contextids) {
|
||||
global $DB;
|
||||
$this->records = array();
|
||||
foreach ($contextids as $contextid) {
|
||||
if (!isset($this->records[$contextid])) {
|
||||
$this->records[$contextid] = context::instance_by_id($contextid);
|
||||
}
|
||||
$parents = $this->records[$contextid]->get_parent_contexts();
|
||||
foreach ($parents as $parentcontext) {
|
||||
if (!isset($this->records[$parentcontext->id])) {
|
||||
$this->records[$parentcontext->id] = $parentcontext;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->make_list_item_instances_from_records($stringidentifier, $link);
|
||||
}
|
||||
|
||||
public function render($roottorender = null) {
|
||||
if ($roottorender === null) {
|
||||
$roottorender = $this->root_node();
|
||||
}
|
||||
$rootitem = html_writer::tag('li', $roottorender->render());
|
||||
return html_writer::tag('ul', $rootitem);
|
||||
}
|
||||
|
||||
public function root_node () {
|
||||
return $this->get_instance(context_system::instance()->id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class qtype_ddmarker_category_list extends qtype_ddmarker_list {
|
||||
protected $contextlist;
|
||||
protected function new_list_item($stringidentifier, $link, $record) {
|
||||
return new qtype_ddmarker_category_list_item($stringidentifier, $link, $record, $this, $this->contextlist);
|
||||
}
|
||||
public function __construct($stringidentifier, $link, $contextids, $contextlist) {
|
||||
global $DB;
|
||||
$this->contextlist = $contextlist;
|
||||
// Probably most efficient way to reconstruct question category tree is
|
||||
// to load all q cats in relevant contexts.
|
||||
list($sql, $params) = $DB->get_in_or_equal($contextids);
|
||||
$this->records = $DB->get_records_select('question_categories', "contextid ".$sql, $params);
|
||||
$this->make_list_item_instances_from_records($stringidentifier, $link);
|
||||
}
|
||||
}
|
||||
|
||||
class qtype_ddmarker_question_list extends qtype_ddmarker_list {
|
||||
protected $categorylist;
|
||||
protected function new_list_item($stringidentifier, $link, $record) {
|
||||
return new qtype_ddmarker_question_list_item($stringidentifier, $link, $record, $this, $this->categorylist);
|
||||
}
|
||||
public function __construct($stringidentifier, $link, $questions, $categorylist) {
|
||||
global $DB;
|
||||
$this->categorylist = $categorylist;
|
||||
$this->records = $questions;
|
||||
$this->make_list_item_instances_from_records($stringidentifier, $link);
|
||||
}
|
||||
public function prepare_for_processing($top) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?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
|
||||
* @subpackage 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);
|
||||
|
||||
/**
|
||||
* 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,195 @@
|
||||
<?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
|
||||
* @subpackage 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;
|
||||
}
|
||||
|
||||
}
|
||||
class qtype_ddmarker_list_renderer extends plugin_renderer_base {
|
||||
|
||||
public function render_qtype_ddmarker_list(qtype_ddmarker_list_item $top) {
|
||||
$list = html_writer::tag('ul', html_writer::tag('li', $this->render_qtype_ddmarker_list_item($top)));
|
||||
return $this->output->container($list, 'listofquestions');
|
||||
}
|
||||
public function render_qtype_ddmarker_list_item(qtype_ddmarker_list_item $listitem) {
|
||||
return $this->item($listitem).$this->children($listitem);
|
||||
}
|
||||
|
||||
public function item(qtype_ddmarker_list_item $item) {
|
||||
global $PAGE;
|
||||
$a = new stdClass();
|
||||
$a->qcount = $item->get_q_count();
|
||||
$a->name = $item->item_name();
|
||||
$thisitem = get_string('listitem'.$item->get_string_identifier().$item->get_list_type(), 'qtype_ddmarker', $a);
|
||||
if ($item->get_linked()) {
|
||||
$actionurl = new moodle_url($PAGE->url, array($item->id_param_name() => $item->get_id()));
|
||||
$thisitem = html_writer::tag('a', $thisitem, array('href' => $actionurl));
|
||||
}
|
||||
return $thisitem;
|
||||
}
|
||||
|
||||
protected function children(qtype_ddmarker_list_item $item) {
|
||||
$children = array();
|
||||
foreach ($item->get_children() as $child) {
|
||||
$children[] = $this->render_qtype_ddmarker_list_item($child);
|
||||
}
|
||||
return html_writer::alist($children);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Admin settings 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();
|
||||
|
||||
$settings = new admin_externalpage('qtypeddmarkerfromimagetarget',
|
||||
get_string('imagetargetconverter', 'qtype_ddmarker'),
|
||||
new moodle_url('/question/type/ddmarker/imagetargetconverter.php'),
|
||||
'moodle/question:config');
|
||||
@@ -0,0 +1,494 @@
|
||||
<?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 {
|
||||
|
||||
protected $error = false;
|
||||
|
||||
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();
|
||||
|
||||
abstract public function center_point();
|
||||
|
||||
protected function is_only_numbers() {
|
||||
$args = func_get_args();
|
||||
foreach ($args as $arg) {
|
||||
if (0 === preg_match('!^[0-9]+$!', $arg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
public static function name() {
|
||||
return substr(get_called_class(), strlen(self::$classnameprefix));
|
||||
}
|
||||
|
||||
protected static $classnameprefix = 'qtype_ddmarker_shape_';
|
||||
|
||||
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;
|
||||
}
|
||||
public static function exists($shape) {
|
||||
return class_exists((self::$classnameprefix).$shape);
|
||||
}
|
||||
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 {
|
||||
protected $width;
|
||||
protected $height;
|
||||
protected $xleft;
|
||||
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 {
|
||||
|
||||
protected $xcentre;
|
||||
protected $ycentre;
|
||||
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 {
|
||||
public $x;
|
||||
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 {
|
||||
public $a;
|
||||
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,127 @@
|
||||
.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;
|
||||
-moz-box-shadow: 3px 3px 4px #000;
|
||||
-webkit-box-shadow: 3px 3px 4px #000;
|
||||
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 @@
|
||||
@ou @ou_vle @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 @@
|
||||
@ou @ou_vle @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,125 @@
|
||||
<?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.
|
||||
$style =
|
||||
$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', '0');
|
||||
target.style.setProperty('height', '0');
|
||||
}())");
|
||||
|
||||
$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 @@
|
||||
@ou @ou_vle @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 @@
|
||||
@ou @ou_vle @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 @@
|
||||
@ou @ou_vle @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 @@
|
||||
@ou @ou_vle @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
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
File diff suppressed because one or more lines are too long
@@ -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/>.
|
||||
|
||||
/**
|
||||
* Test helpers for the drag-and-drop markers question type.
|
||||
*
|
||||
* @package qtype
|
||||
* @subpackage 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,142 @@
|
||||
<?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,39 @@
|
||||
<?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 = 2015031800;
|
||||
$plugin->requires = 2013101800;
|
||||
$plugin->cron = 0;
|
||||
$plugin->component = 'qtype_ddmarker';
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
$plugin->release = '1.8 for Moodle 2.5+';
|
||||
|
||||
$plugin->dependencies = array(
|
||||
'qtype_gapselect' => 2014111200,
|
||||
'qtype_ddimageortext' => 2014111200,
|
||||
);
|
||||
|
||||
$plugin->outestssufficient = true;
|
||||
Vendored
+607
@@ -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"]});
|
||||
+2
File diff suppressed because one or more lines are too long
+607
@@ -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"]});
|
||||
Vendored
+277
@@ -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"]});
|
||||
Vendored
+1
@@ -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"]});
|
||||
+277
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+272
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user