diff --git a/mod/hotpot/README.TXT b/mod/hotpot/README.TXT index 872b041ac71..4906193a1a6 100644 --- a/mod/hotpot/README.TXT +++ b/mod/hotpot/README.TXT @@ -1,5 +1,10 @@ -This is v2.0.8 of the HotPot module - It has been tested on Moodle 1.1 thru 1.5, MySQL and PostGres7 databases and PHP 4.1 thru 5.0.4 +This is v2.1.0 of the HotPot module + This module allows teachers to administer Hot Potatoes and TexToys quizzes via Moodle. + It has been tested on: + - Hot Potatoes 6 + - Moodle 1.1 thru 1.5 + - PHP 4.1 thru 5.0.4 + - MySQL and PostgreSQL databases This module may be distributed under the terms of the General Public License (see http://www.gnu.org/licenses/gpl.txt for details) @@ -9,7 +14,7 @@ This is v2.0.8 of the HotPot module ================ IMPORTANT NOTICE ================ -* Please be sure to use Hot Potatoes according to the conditions of use which are listed at the end of this file. If you restrict use via a required Moodle login, you most likely can still meet the 'freely available' condition if you make the same material on a separate URL that permits free access. Otherwise, please purchase a license. +* Please be sure to use Hot Potatoes according to the conditions of use which are listed at the end of this file. If you restrict use via a required Moodle login, you most likely can still meet the 'freely available' condition if you make the same material on a separate URL that permits free access. Otherwise, please purchase a license. TO INSTALL OR UPDATE THIS MODULE @@ -121,13 +126,13 @@ HOT POTATOES CONDITIONS OF USE **Reproduced from the Hot Potatoes site** Hot Potatoes is offered free to the educational community by the University of Victoria Humanities Computing and Media Centre (formerly the Language Centre), under certain conditions. Hot Potatoes is free for use by state educational institutions which are non-profit making, on the condition that the material produced using the program is freely available to anyone via the WWW. However, you need to purchase a licence under any of the following conditions: -? You do not work for a public sector educational establishment. -? You charge money for access to the material you make with Hot Potatoes. -? You restrict access to the material in some way. (The only exception here is if you have an account on www.hotpot.net, where you ARE allowed to use password restrictions.) -? You want to use the Masher program included with the Hot Potatoes suite. +* You do not work for a public sector educational establishment. +* You charge money for access to the material you make with Hot Potatoes. +* You restrict access to the material in some way. (The only exception here is if you have an account on www.hotpot.net, where you ARE allowed to use password restrictions.) +* You want to use the Masher program included with the Hot Potatoes suite. For more information on licences, and details on how to purchase one, check out our Website at: - http://www.halfbakedsoftware.com/hotpot/ + http://www.halfbakedsoftware.com/hotpot/ If you intend using the programs to generate more than a handful of exercises, please make sure you register. This costs you nothing -- see How to register for details. -Martin Holmes, Half-Baked Software and the University of Victoria HCMC, 1998-2004. \ No newline at end of file +Martin Holmes, Half-Baked Software and the University of Victoria HCMC, 1998-2004. diff --git a/mod/hotpot/attempt.php b/mod/hotpot/attempt.php index 56ce07c615c..dbc5af6a9ff 100644 --- a/mod/hotpot/attempt.php +++ b/mod/hotpot/attempt.php @@ -2,8 +2,9 @@ require_once("../../config.php"); require_once("lib.php"); - $next_url = ""; $msg = ''; + $next_url = ""; + $quiz_is_finished = true; $attemptid = required_param("attemptid"); if (is_numeric($attemptid)) { @@ -33,50 +34,132 @@ // make sure this user is enrolled in this course require_login($course->id); - if ($attempt->timefinish && false) { + $time = time(); + $msg = get_string('resultssaved', 'hotpot'); - $msg = 'This attempt has already been submitted'; + // update attempt record fields using incoming data + $attempt->score = optional_param('mark', NULL, PARAM_INT); + $attempt->status = optional_param('status', NULL, PARAM_INT); + $attempt->details = optional_param('detail', NULL, PARAM_RAW); + $attempt->endtime = optional_param('endtime', NULL, PARAM_ALPHA); + $attempt->starttime = optional_param('starttime', NULL, PARAM_ALPHA); + $attempt->timefinish = $time; + if ($attempt->endtime) { + $attempt->endtime = strtotime($attempt->endtime); + } + if ($attempt->starttime) { + $attempt->starttime = strtotime($attempt->starttime); + } + + // set clickreportid, (for click reporting) + $attempt->clickreportid = $attempt->id; + + if (empty($attempt->status)) { + if (empty($attempt->endtime)) { + $attempt->status = HOTPOT_STATUS_INPROGRESS; + } else { + $attempt->status = HOTPOT_STATUS_COMPLETED; + } + } + + // for the rare case where a quiz was "in progress" during an update from hotpot v1 to v2 + if (empty($attempt->timestart) && !empty($attempt->starttime)) { + $attempt->timestart = $attempt->starttime; + } + + + // check if this is the second (or subsequent) click + if (get_field("hotpot_attempts", "timefinish", "id", $attempt->id)) { + + if ($hotpot->clickreporting==HOTPOT_YES) { + // add attempt record for each form submission + // records are linked via the "clickreportid" field + + // update status in previous records in this group + set_field("hotpot_attempts", "status", $attempt->status, "clickreportid", $attempt->clickreportid); + + // add new attempt record + unset ($attempt->id); + $attempt->id = insert_record("hotpot_attempts", $attempt); + + if (empty($attempt->id)) { + error("Could not insert attempt record: ".$db->ErrorMsg(), $next_url); + } + + // add attempt details record, if necessary + if (!empty($attempt->details)) { + unset($details); + $details->attempt = $attempt->id; + $details->details = $attempt->details; + if (! insert_record("hotpot_details", $details, false)) { + error("Could not insert attempt details record: ".$db->ErrorMsg(), $next_url); + } + } + + } else { + // remove previous responses for this attempt + // (N.B. this does NOT remove the attempt record, just the responses) + $ok = delete_records("hotpot_responses", "attempt", $attempt->id); + } + } + + // remove slashes added by lib/setup.php + $attempt->details = stripslashes($attempt->details); + + // add details of this attempt + hotpot_add_attempt_details($attempt); + + // add slashes again, so the details can be added to the database + $attempt->details = addslashes($attempt->details); + + // update the attempt record + if (! update_record("hotpot_attempts", $attempt)) { + error("Could not update attempt record: ".$db->ErrorMsg(), $next_url); + } + + // get previous attempt details record, if any + $details_exist = record_exists("hotpot_details", "attempt", $attempt->id); + + // delete/update/add the attempt details record + if (empty($attempt->details)) { + if ($details_exist) { + delete_records("hotpot_details", "attempt", $attempt->id); + } } else { - $time = time(); - $msg = get_string('resultssaved', 'hotpot'); - - $attempt->score = isset($_POST['mark']) ? $_POST['mark'] : NULL; - $attempt->details = isset($_POST['detail']) ? $_POST['detail'] : NULL; - $attempt->endtime = isset($_POST['endtime']) ? strtotime($_POST['endtime']) : NULL; - $attempt->starttime = isset($_POST['starttime']) ? strtotime($_POST['starttime']) : NULL; - $attempt->timefinish = $time; - - // for the rare case where a quiz was "in progress" during an update from hotpot v1 to v2 - if (empty($attempt->timestart) && !empty($attempt->starttime)) { - $attempt->timestart = $attempt->starttime; + if ($details_exist) { + set_field("hotpot_details", "details", $attempt->details, "attempt", $attempt->id); + } else { + unset($details); + $details->attempt = $attempt->id; + $details->details = $attempt->details; + if (! insert_record("hotpot_details", $details)) { + error("Could not insert attempt details record: ".$db->ErrorMsg(), $next_url); + } } + } - // remove slashes added by lib/setup.php - $attempt->details = stripslashes($attempt->details); + if ($attempt->status==HOTPOT_STATUS_INPROGRESS) { + $quiz_is_finished = false; - // add details of this attempt - hotpot_add_attempt_details($attempt); - - // add slashes again, so the details can be added to the database - $attempt->details = addslashes($attempt->details); - - if (! update_record("hotpot_attempts", $attempt)) { - error("Could not update attempt record: ".$db->ErrorMsg(), $next_url); - } - - // set previously unfinished attempts of this quiz by this user to "finished" - hotpot_close_previous_attempts($hotpot->id, $USER->id, $time); + } else { // quiz is finished add_to_log($course->id, "hotpot", "submit", "review.php?id=$cm->id&attempt=$attempt->id", "$hotpot->id", "$cm->id"); - } - if ($hotpot->shownextquiz==HOTPOT_YES && is_numeric($next_cm = hotpot_get_next_cm($cm))) { - $next_url = "$CFG->wwwroot/mod/hotpot/view.php?id=$next_cm"; + + if ($hotpot->shownextquiz==HOTPOT_YES && is_numeric($next_cm = hotpot_get_next_cm($cm))) { + $next_url = "$CFG->wwwroot/mod/hotpot/view.php?id=$next_cm"; + } } } - // redirect to the next quiz or the course page - redirect($next_url, $msg); + if ($quiz_is_finished) { + // redirect to the next quiz or the course page + redirect($next_url, $msg); + } else { + // continue the quiz + header("Status: 204"); + header("HTTP/1.0 204 No Response"); + } // ================= diff --git a/mod/hotpot/backuplib.php b/mod/hotpot/backuplib.php index ceab3997ff2..b0f2865fabb 100644 --- a/mod/hotpot/backuplib.php +++ b/mod/hotpot/backuplib.php @@ -6,30 +6,27 @@ // This is the "graphical" structure of the hotpot mod: //----------------------------------------------------------- // - // hotpot - // (CL, pk->id, files) + // hotpot + // (CL, pk->id, + // fk->course, files) // | - // +--------------+--------------+ - // | | - // | | - // hotpot_attempts hotpot_questions - // (UL, pk->id, (UL, pk->id, - // fk->hotpot) fk->hotpot, text) - // | | | - // | | | - // +--------------+--------------+ | - // | | - // | | - // hotpot_responses | - // (UL, pk->id, | - // fk->attempt, question, | - // correct, wrong, ignored) | - // | | - // | | - // +-----------+-----------+ - // | - // hotpot_strings - // (UL, pk->id) + // +--------------+---------------+ + // | | + // hotpot_attempts hotpot_questions + // (UL, pk->id, (UL, pk->id, + // fk->hotpot) fk->hotpot, text) + // | | | + // +-------------------+----------+ | + // | | | + // hotpot_details hotpot_responses | + // (UL, pk->id, (UL, pk->id, | + // fk->attempt) fk->attempt, question, | + // correct, wrong, ignored) | + // | | + // +-------+-------+ + // | + // hotpot_strings + // (UL, pk->id) // // Meaning: pk->primary key field of the table // fk->foreign key to link with parent @@ -39,38 +36,37 @@ // files->table may have files // //----------------------------------------------------------- - // It is not necessary to backup "questions", "responses" - // and "strings", because they can be restored from the - // "details" field of the "attempts" records - //----------------------------------------------------------- function hotpot_backup_mods($bf, $preferences) { + // $bf : resource id for b(ackup) f(ile) + // $preferences : object containing switches and settings for this backup - $level = 3; + $level = 3; $status = true; $table = 'hotpot'; - $field = 'course'; - $value = $preferences->backup_course; - - $modtype = 'hotpot'; + $select = "course=$preferences->backup_course"; $records_tag = ''; $records_tags = array(); $record_tag = 'MOD'; - $record_tags = array('MODTYPE'=>$modtype); + $record_tags = array('MODTYPE'=>'hotpot'); $excluded_tags = array(); $more_backup = ''; - if ($preferences->mods[$modtype]->userinfo) { - $more_backup .= $modtype.'_backup_attempts($bf, $record, $level, $status);'; + if ($preferences->mods['hotpot']->userinfo) { + $more_backup .= '$GLOBALS["hotpot_backup_string_ids"] = array();'; + $more_backup .= '$status = hotpot_backup_attempts($bf, $record, $level, $status);'; + $more_backup .= '$status = hotpot_backup_questions($bf, $record, $level, $status);'; + $more_backup .= '$status = hotpot_backup_strings($bf, $record, $level, $status);'; + $more_backup .= 'unset($GLOBALS["hotpot_backup_string_ids"]);'; // tidy up } return hotpot_backup_records( $bf, $status, $level, - $table, $field, $value, + $table, $select, $records_tag, $records_tags, $record_tag, $record_tags, $excluded_tags, $more_backup @@ -80,8 +76,7 @@ // $parent is a reference to a hotpot record $table = 'hotpot_attempts'; - $field = 'hotpot'; - $value = $parent->id; + $select = "hotpot=$parent->id"; $records_tag = 'ATTEMPT_DATA'; $records_tags = array(); @@ -90,25 +85,175 @@ $record_tags = array(); $more_backup = ''; - $excluded_tags = array(); + $more_backup .= 'hotpot_backup_details($bf, $record, $level, $status);'; + $more_backup .= 'hotpot_backup_responses($bf, $record, $level, $status);'; + + $excluded_tags = array('hotpot'); return hotpot_backup_records( $bf, $status, $level, - $table, $field, $value, + $table, $select, $records_tag, $records_tags, $record_tag, $record_tags, $excluded_tags, $more_backup ); } + function hotpot_backup_details($bf, &$parent, $level, $status) { + // $parent is a reference to an attempt record - function hotpot_backup_records(&$bf, $status, $level, $table, $field, $value, $records_tag, $records_tags, $record_tag, $record_tags, $excluded_tags, $more_backup) { + $table = 'hotpot_details'; + $select = "attempt=$parent->id"; + + $records_tag = ''; + $records_tags = array(); + + $record_tag = ''; + $record_tags = array(); + + $more_backup = ''; + $excluded_tags = array('id','attempt'); + + return hotpot_backup_records( + $bf, $status, $level, + $table, $select, + $records_tag, $records_tags, + $record_tag, $record_tags, + $excluded_tags, $more_backup + ); + } + function hotpot_backup_responses($bf, &$parent, $level, $status) { + // $parent is a reference to an attempt record + + $table = 'hotpot_responses'; + $select = "attempt=$parent->id"; + + $records_tag = 'RESPONSE_DATA'; + $records_tags = array(); + + $record_tag = 'RESPONSE'; + $record_tags = array(); + + $more_backup = 'hotpot_backup_string_ids($record, array("correct","wrong","ignored"));'; + $excluded_tags = array('id','attempt'); + + return hotpot_backup_records( + $bf, $status, $level, + $table, $select, + $records_tag, $records_tags, + $record_tag, $record_tags, + $excluded_tags, $more_backup + ); + } + function hotpot_backup_questions($bf, &$parent, $level, $status) { + // $parent is a reference to an hotpot record + + $table = 'hotpot_questions'; + $select = "hotpot=$parent->id"; + + $records_tag = 'QUESTION_DATA'; + $records_tags = array(); + + $record_tag = 'QUESTION'; + $record_tags = array(); + + $more_backup = 'hotpot_backup_string_ids($record, array("text"));'; + $excluded_tags = array('hotpot'); + + return hotpot_backup_records( + $bf, $status, $level, + $table, $select, + $records_tag, $records_tags, + $record_tag, $record_tags, + $excluded_tags, $more_backup + ); + } + function hotpot_backup_string_ids(&$record, $fields) { + // as the questions and responses tables are backed up + // this function is called to store the ids of strings. + // The string ids are used later by "hotpot_backup_strings" + // $GLOBALS['hotpot_backup_string_ids'] was initialized in "hotpot_backup_mods" + + // store the ids of strings used in this $record's $fields + foreach ($fields as $field) { + if (empty($record->$field)) { + // do nothing + } else { + $value = $record->$field; + $ids = explode(',', "$value"); + foreach ($ids as $id) { + if (empty($id)) { + // do nothing + } else { + $GLOBALS['hotpot_backup_string_ids'][$id] = true; + } + } + } + } + } + function hotpot_backup_strings($bf, $record, $level, $status) { + // This functions backups the strings used + // in the question and responses for a single hotpot activity + // The ids of the strings were stored by "hotpot_backup_string_ids" + // $GLOBALS['hotpot_backup_string_ids'] was initialized in "hotpot_backup_mods" + + // retrieve $ids of strings to be backed up + $ids = array_keys($GLOBALS['hotpot_backup_string_ids']); + + if (empty($ids)) { + // no strings to backup + } else { + + sort($ids); + $ids = implode(',', $ids); + + $table = 'hotpot_strings'; + $select = "id IN ($ids)"; + + $records_tag = 'STRING_DATA'; + $records_tags = array(); + + $record_tag = 'STRING'; + $record_tags = array(); + + $more_backup = ''; + $excluded_tags = array(''); + + $status = hotpot_backup_records( + $bf, $status, $level, + $table, $select, + $records_tag, $records_tags, + $record_tag, $record_tags, + $excluded_tags, $more_backup + ); + } + return $status; + } + + function hotpot_backup_records(&$bf, $status, $level, $table, $select, $records_tag, $records_tags, $record_tag, $record_tags, $excluded_tags, $more_backup) { + // general purpose backup function + + // $bf : resource id of backup file + // $status : current status of backup (true or false) + // $level : current depth level in the backup XML tree + + // $table : table from which records will be selected and backed up + // $select : SQL selection string + + // $records_tag : optional XML tag which starts a group of records (and descends a level) + // $records_tags : optional XML tags to be inserted at the start of a group of records + + // $record_tag : optional XML tag which starts a record (and descends a level) + // $record_tags : optional XML tags to be inserted at the start of a record + + // $excluded_tags : fields which will NOT be backed up from the records + // $more_backup : optional PHP code to be eval(uated) for each record // If any of the "fwrite" statements fail, // no further "fwrite"s will be attempted // and the function returns "false". // Otherwise, the function returns "true". - if ($status && ($records = get_records($table, $field, $value, 'id'))) { + if ($status && ($records = get_records_select($table, $select, 'id'))) { // start a group of records if ($records_tag) { diff --git a/mod/hotpot/db/mysql.php b/mod/hotpot/db/mysql.php index 43eca1350c3..002d989b12c 100644 --- a/mod/hotpot/db/mysql.php +++ b/mod/hotpot/db/mysql.php @@ -15,6 +15,12 @@ function hotpot_upgrade($oldversion) { $ok = $ok && hotpot_update_to_v2_from_v1(); } + // update to HotPot v2.1 + if ($oldversion < 2005090700) { + $ok = $ok && hotpot_get_update_to_v2(); + $ok = $ok && hotpot_update_to_v2_1(); + } + return $ok; } function hotpot_get_update_to_v2() { diff --git a/mod/hotpot/db/mysql.sql b/mod/hotpot/db/mysql.sql index beab33ca162..49887bcd1f1 100644 --- a/mod/hotpot/db/mysql.sql +++ b/mod/hotpot/db/mysql.sql @@ -4,7 +4,7 @@ CREATE TABLE prefix_hotpot ( id int(10) unsigned NOT NULL auto_increment, - course int(10) unsigned NOT NULL, + course int(10) unsigned NOT NULL default '0', name varchar(255) NOT NULL default '', reference varchar(255) NOT NULL default '', summary text NOT NULL, @@ -23,6 +23,9 @@ CREATE TABLE prefix_hotpot ( forceplugins int(4) unsigned NOT NULL default '0', password varchar(255) NOT NULL default '', subnet varchar(255) NOT NULL default '', + clickreporting tinyint(4) unsigned NOT NULL default '0', + studentfeedback tinyint(4) unsigned NOT NULL default '0', + studentfeedbackurl varchar(255) default NULL, PRIMARY KEY (id) ) TYPE=MyISAM COMMENT='details about Hot Potatoes quizzes'; @@ -32,19 +35,33 @@ CREATE TABLE prefix_hotpot ( CREATE TABLE prefix_hotpot_attempts ( id int(10) unsigned NOT NULL auto_increment, - hotpot int(10) unsigned NOT NULL, - userid int(10) unsigned NOT NULL, + hotpot int(10) unsigned NOT NULL default '0', + userid int(10) unsigned NOT NULL default '0', starttime int(10) unsigned default NULL, endtime int(10) unsigned default NULL, score int(6) unsigned default NULL, penalties int(6) unsigned default NULL, attempt int(6) unsigned NOT NULL default '0', - details text, timestart int(10) unsigned default NULL, timefinish int(10) unsigned default NULL, - PRIMARY KEY (id) + status tinyint(4) unsigned NOT NULL default '1', + clickreportid int(10) unsigned default NULL, + PRIMARY KEY (id), + KEY prefix_hotpot_attempts_hotpot_idx (hotpot), + KEY prefix_hotpot_attempts_userid_idx (userid) ) TYPE=MyISAM COMMENT='details about Hot Potatoes quiz attempts'; +# +# Table structure for table `hotpot_details` +# + +CREATE TABLE prefix_hotpot_details ( + id int(10) unsigned NOT NULL auto_increment, + attempt int(10) unsigned NOT NULL, + details text, + PRIMARY KEY (id), + KEY prefix_hotpot_details_attempt_idx (attempt) +) TYPE=MyISAM COMMENT='raw details (as XML) of Hot Potatoes quiz attempts'; # # Table structure for table `hotpot_questions` @@ -52,22 +69,23 @@ CREATE TABLE prefix_hotpot_attempts ( CREATE TABLE prefix_hotpot_questions ( id int(10) unsigned NOT NULL auto_increment, - name varchar(255) NOT NULL default '', - type int(10) unsigned NOT NULL default '0', - text text, + name text NOT NULL, + type tinyint(4) unsigned default NULL, + text int(10) unsigned default NULL, hotpot int(10) unsigned NOT NULL default '0', - PRIMARY KEY (id) + PRIMARY KEY (id), + KEY prefix_hotpot_questions_name_idx (name(20)), + KEY prefix_hotpot_questions_hotpot_idx (hotpot) ) TYPE=MyISAM COMMENT='details about questions in Hot Potatatoes quiz attempts'; - # # Table structure for table `hotpot_responses` # CREATE TABLE prefix_hotpot_responses ( id int(10) unsigned NOT NULL auto_increment, - attempt int(10) unsigned NOT NULL, - question int(10) unsigned NOT NULL, + attempt int(10) unsigned NOT NULL default '0', + question int(10) unsigned NOT NULL default '0', score smallint(8) default NULL, weighting smallint(8) default NULL, correct varchar(255) default NULL, @@ -76,7 +94,9 @@ CREATE TABLE prefix_hotpot_responses ( hints smallint(6) default NULL, clues smallint(6) default NULL, checks smallint(6) default NULL, - PRIMARY KEY (id) + PRIMARY KEY (id), + KEY prefix_hotpot_responses_attempt_idx (attempt), + KEY prefix_hotpot_responses_question_idx (question) ) TYPE=MyISAM COMMENT='details about responses in Hot Potatatoes quiz attempts'; # @@ -86,6 +106,7 @@ CREATE TABLE prefix_hotpot_responses ( CREATE TABLE prefix_hotpot_strings ( id int(10) unsigned NOT NULL auto_increment, string text NOT NULL, - PRIMARY KEY (id) + PRIMARY KEY (id), + KEY prefix_hotpot_strings_string_idx (string(20)) ) TYPE=MyISAM COMMENT='strings used in Hot Potatatoes questions and responses'; - + \ No newline at end of file diff --git a/mod/hotpot/db/postgres7.php b/mod/hotpot/db/postgres7.php index d9e639391b5..14e8437bd67 100644 --- a/mod/hotpot/db/postgres7.php +++ b/mod/hotpot/db/postgres7.php @@ -1,6 +1,8 @@ dbtype)) { + case 'mysql' : + $NOT_REGEXP = 'NOT REGEXP'; + break; + case 'postgres7' : + $NOT_REGEXP = '!~'; + break; + default: + $NOT_REGEXP = ''; + break; + } + if ($NOT_REGEXP) { + $ok = $ok && execute_sql("UPDATE {$CFG->prefix}hotpot_questions SET text=NULL WHERE text $NOT_REGEXP '^[0-9]+$'"); + } + + // hotpot_questions: change type of "text" field to "INT(10)" + $ok = $ok && hotpot_db_update_field_type('hotpot_questions', 'text', 'text', 'INTEGER', 10, 'UNSIGNED', 'NULL'); + + // hotpot_attempts + + // hotpot_attempts: create and set status field (1=in-progress, 2=timed-out, 3=abandoned, 4=completed) + $ok = $ok && hotpot_db_update_field_type('hotpot_attempts', '', 'status', 'INTEGER', 4, 'UNSIGNED', 'NOT NULL', 1); + $ok = $ok && execute_sql("UPDATE {$CFG->prefix}hotpot_attempts SET status=1 WHERE timefinish=0 AND SCORE IS NULL"); + $ok = $ok && execute_sql("UPDATE {$CFG->prefix}hotpot_attempts SET status=3 WHERE timefinish>0 AND SCORE IS NULL"); + $ok = $ok && execute_sql("UPDATE {$CFG->prefix}hotpot_attempts SET status=4 WHERE timefinish>0 AND SCORE IS NOT NULL"); + + // hotpot_attempts: create and set clickreport fields + $ok = $ok && hotpot_db_update_field_type('hotpot', '', 'clickreporting', 'INTEGER', 4, 'UNSIGNED', 'NOT NULL', 0); + $ok = $ok && hotpot_db_update_field_type('hotpot_attempts', '', 'clickreportid', 'INTEGER', 10, 'UNSIGNED', 'NULL'); + $ok = $ok && execute_sql("UPDATE {$CFG->prefix}hotpot_attempts SET clickreportid=id WHERE clickreportid IS NULL"); + + // hotpot_attempts: create and set studentfeedback field (0=none, 1=formmail, 2=moodleforum, 3=moodlemessaging) + $ok = $ok && hotpot_db_update_field_type('hotpot', '', 'studentfeedback', 'INTEGER', 4, 'UNSIGNED', 'NOT NULL', '0'); + $ok = $ok && hotpot_db_update_field_type('hotpot', '', 'studentfeedbackurl', 'VARCHAR', 255, '', 'NULL'); + + // hotpot_attempts: move "details" to separate table + $table = 'hotpot_details'; + if (hotpot_db_table_exists($table)) { + // do nothing + } else { + $ok = $ok && hotpot_create_table($table); + switch (strtolower($CFG->dbtype)) { + case 'mysql' : + case 'postgres7' : + $sql = " + INSERT INTO {$CFG->prefix}$table (attempt, details) + SELECT a.id AS attempt, a.details AS details + FROM {$CFG->prefix}hotpot_attempts AS a + WHERE + a.details IS NOT NULL AND a.details <> '' + AND a.details LIKE '' + "; + break; + default: + $sql = ''; + break; + } + if ($sql) { + $ok = $ok && execute_sql($sql); + } + } + + // hotpot_attempts: remove the "details" field + $ok = $ok && hotpot_db_remove_field('hotpot_attempts', 'details'); + + // add indexes + $ok = $ok && hotpot_db_add_index('hotpot_attempts', 'hotpot'); + $ok = $ok && hotpot_db_add_index('hotpot_attempts', 'userid'); + $ok = $ok && hotpot_db_add_index('hotpot_details', 'attempt'); + $ok = $ok && hotpot_db_add_index('hotpot_questions', 'name', 20); + $ok = $ok && hotpot_db_add_index('hotpot_questions', 'hotpot'); + $ok = $ok && hotpot_db_add_index('hotpot_responses', 'attempt'); + $ok = $ok && hotpot_db_add_index('hotpot_responses', 'question'); + $ok = $ok && hotpot_db_add_index('hotpot_strings', 'string', 20); + + // hotpot_string: correct double-encoded HTML entities + $ok = $ok && execute_sql(" + UPDATE {$CFG->prefix}hotpot_strings + SET string = REPLACE(string, '&','&') + WHERE string LIKE '%&#%' + AND (string LIKE '<' OR string LIKE '>') + "); + + // hotpot_question: remove questions which refer to deleted hotpots + if ($ok) { + // try and get all hotpot records + if ($records = get_records('hotpot')) { + $ids = implode(',', array_keys($records)); + $sql = "DELETE FROM {$CFG->prefix}hotpot_questions WHERE hotpot NOT IN ($ids)"; + } else { + // remove all question records (because there are no valid hotpot ids) + $sql = "TRUNCATE {$CFG->prefix}hotpot_questions"; + } + print "Removing unused question records ..."; + execute_sql($sql); + } + + if ($ok) { + // remove old 'v6' templates folder (replaced by 'template' folder) + $ds = DIRECTORY_SEPARATOR; + $dir = "mod{$ds}hotpot{$ds}v6"; + print "removing old templates ($dir) ... "; + $ok = hotpot_rm("$CFG->dirroot{$ds}$dir", false); + print $ok ? get_string('success') : 'failed'; + } + + return $ok; +} function hotpot_update_to_v2_from_v1() { global $CFG; $ok = true; @@ -609,6 +728,104 @@ function hotpot_update_print_warning($field, $value, $table, $id) { // database functions /////////////////////////// +function hotpot_db_index_exists($table, $index, $feedback=false) { + global $CFG, $db; + $exists = false; + + // save and switch off SQL message echo + $debug = $db->debug; + $db->debug = $feedback; + + switch (strtolower($CFG->dbtype)) { + case 'mysql' : + $rs = $db->Execute("SHOW INDEX FROM `$table`"); + if ($rs && $rs->RecordCount()>0) { + $records = $rs->GetArray(); + foreach ($records as $record) { + if (isset($record['Key_name']) && $record['Key_name']==$index) { + $exists = true; + break; + } + } + } + break; + + case 'postgres7' : + $rs = $db->Execute("SELECT relname FROM pg_class WHERE relname = '$index' AND relkind='i'"); + if ($rs && $rs->RecordCount()>0) { + $exists = true; + } + break; + } + + // restore SQL message echo + $db->debug = $debug; + + return $exists; +} +function hotpot_db_delete_index($table, $index, $feedback=false) { + global $CFG, $db; + $ok = true; + + // check index exists + if (hotpot_db_index_exists($table, $index)) { + + switch (strtolower($CFG->dbtype)) { + case 'mysql' : + $sql = "ALTER TABLE `$table` DROP INDEX `$index`"; + break; + + case 'postgres7' : + $sql = "DROP INDEX $index"; + break; + + default: // unknown database type + $sql = ''; + break; + } + if ($sql) { + // save and switch off SQL message echo + $debug = $db->debug; + $db->debug = $feedback; + + $ok = $db->Execute($sql) ? true : false; + + // restore SQL message echo + $db->debug = $debug; + + } else { // unknown database type + $ok = false; + } + } + return $ok; +} +function hotpot_db_add_index($table, $field, $length='') { + global $CFG, $db; + + // expand $table and $index names + $table = "{$CFG->prefix}$table"; + $index = "{$table}_{$field}_idx"; + + // delete $index if it already exists + $ok = hotpot_db_delete_index($table, $index); + + switch (strtolower($CFG->dbtype)) { + case 'mysql' : + $length = empty($length) ? '' : " ($length)"; + $ok = $ok && $db->Execute("ALTER TABLE `$table` ADD INDEX `$index` (`$field`$length)"); + break; + + case 'postgres7' : + $ok = $ok && $db->Execute("CREATE INDEX $index ON $table ($field)"); + break; + + default: // unknown database type + $ok = false; + break; + } + + return $ok; +} function hotpot_db_table_exists($table, $feedback=false) { return hotpot_db_object_exists($table, '', $feedback); } @@ -766,7 +983,7 @@ function hotpot_db_update_field_type($table, $oldfield, $field, $type, $size, $u } if (empty($oldfield) && hotpot_db_field_exists($table, $field)) { $oldfield = $field; - } + } if (is_string($unsigned)) { $unsigned = (strtoupper($unsigned)=='UNSIGNED'); } @@ -901,7 +1118,7 @@ function hotpot_db_update_field_type($table, $oldfield, $field, $type, $size, $u // transfer $oldfield values, if necessary if ( $oldfield != '""' ) { - execute_sql("UPDATE $table SET $tmpfield = $oldfield"); + execute_sql("UPDATE $table SET $tmpfield = CAST ($oldfield AS $fieldtype)"); execute_sql("ALTER TABLE $table DROP COLUMN $oldfield"); } @@ -963,5 +1180,39 @@ function hotpot_db_update_record($table, $record, $forcenull=false) { } return $ok; } +function hotpot_rm($target, $output=true) { + $ok = true; + if (!empty($target)) { + if (is_file($target)) { + if ($output) { + print "removing file: $target ... "; + } + $ok = @unlink($target); + + } else if (is_dir($target)) { + $dir = dir($target); + while(false !== ($entry = $dir->read())) { + if ($entry!='.' && $entry!='..') { + $ok = $ok && hotpot_rm($target.DIRECTORY_SEPARATOR.$entry, $output); + } + } + $dir->close(); + if ($output) { + print "removing folder: $target ... "; + } + $ok = $ok && @rmdir($target); + } else { // not a file or directory (probably doesn't exist) + $output = false; + } + if ($output) { + if ($ok) { + print 'OK
'; + } else { + print 'Failed
'; + } + } + } + return $ok; +} ?> diff --git a/mod/hotpot/hotpot-full.js b/mod/hotpot/hotpot-full.js index 9f7ed8a0783..031aa966a61 100644 --- a/mod/hotpot/hotpot-full.js +++ b/mod/hotpot/hotpot-full.js @@ -171,9 +171,12 @@ if (window.JCloze==null) { JCloze[1] = true; // show student's correct answer JCloze[2] = true; // show other correct answer(s), if any JCloze[3] = true; // show wrong answer(s), if any (NOT available for v5) - JCloze[4] = true; // show number of hints or penalties - JCloze[5] = true; // show if clue was asked for or not + JCloze[4] = false; // show number of hints + checks (legacy field, replaced by [7]+[9]) + JCloze[5] = false; // show if clue was asked for or not (legacy field, replaced by [8]) JCloze[6] = true; // show clue + JCloze[7] = true; // show number of hints (=next letter requests) + JCloze[8] = true; // show number of clues + JCloze[9] = true; // show number of checks } // JCloze quizzes use the global variables 'I' and 'State' @@ -214,8 +217,15 @@ if (window.JCross==null) { JCross[0] = true; // show separator line between answers on email JCross[1] = true; // show number of penalties (hints or checks before complete) JCross[2] = true; // show number of letters - JCross[3] = true; // show answers + JCross[3] = true; // show correct answers JCross[4] = true; // show clues + + JCross[5] = true; // show wrong answers + JCross[6] = true; // show if clue was asked for or not + JCross[7] = true; // show number of hints (=next letter requests) + JCross[8] = true; // show number of checks + + // there are no "ignored" answers for JCross quizzes } // JCross quizzes use the following global variables: @@ -239,9 +249,14 @@ if (window.JCross==null) { if (window.JMatch==null) { JMatch = new Array(); JMatch[0] = true; // show separator line between answers on email - JMatch[1] = true; // show number of attempts for each match - JMatch[2] = true; // show LHS texts - JMatch[3] = true; // show RHS texts + JMatch[1] = false; // show number of penalties (= total number of checks) + JMatch[2] = true; // show LHS texts (the question) + JMatch[3] = true; // show correct answers + JMatch[4] = true; // show wrong answers + JMatch[5] = true; // show checks (per match) [empty or unchanged RHS are not counted] + + // JMatch has no "clue" or "hint" buttons + // there cannot be any "ignored" answers } // v5 JMatch quizzes use the global variables 'I' and 'Status' (and 'RItems') @@ -291,10 +306,12 @@ if (window.JMatch==null) { if (window.JMix==null) { JMix = new Array(); JMix[0] = true; // show separator line between answers on email - JMix[1] = true; // show number of wrong guesses + JMix[1] = false; // show number of wrong guesses (replaced by JMix[5]) JMix[2] = true; // show right answer JMix[3] = true; // show wrong answer, if any JMix[4] = false; // show answer as text (false) or number (true) + JMix[5] = true; // show number of checks + JMix[6] = true; // show number of hints (=show next word) } // JMix quizzes use the global variables @@ -317,9 +334,9 @@ if (window.JQuiz==null) { JQuiz[0] = true; // show separator line between answers on email JQuiz[1] = true; // show question text JQuiz[2] = true; // show student's correct answer(s) - JQuiz[3] = true; // show wrong and ignored answer(s) + JQuiz[3] = false; // show wrong and ignored answer(s) (legacy field superceded by [8] & [9]) JQuiz[4] = true; // show number of hints requested - JQuiz[5] = true; // show number of checks of incorrect answers + JQuiz[5] = false; // show number of checks of incorrect answers (legacy field superceded by [12]) // HP6 v6 quizzes only JQuiz[6] = false; // show answer value (false) or A,B,C... index (true) @@ -328,6 +345,8 @@ if (window.JQuiz==null) { JQuiz[9] = true; // show ignored answers (not relevant for multi-select questions) JQuiz[10] = true; // show score weightings JQuiz[11] = true; // show question type + JQuiz[12] = true; // show number of checks (if true, then JQuiz[5] of will be ignored) + JQuiz[13] = true; // show number of times ShowAnswer button was pressed (usually 0 or 1) } // v5 JQuiz quizzes use the global variables 'I' and 'Status' @@ -466,6 +485,40 @@ if (window.MSG==null) { MSG[18] += '\n\n' + 'Tools->Pop-up Blocker->Turn Off Pop-up Blocker'; } } +//if (window.FEEDBACK==null) { +// FEEDBACK = new Array(); +// FEEDBACK[0] = ''; // url of feedback page/script + +// FEEDBACK[1] = ''; // array of array('teachername', 'value'); +// FEEDBACK[2] = ''; // 'student name' [formmail only] +// FEEDBACK[3] = ''; // 'email@somewhere.com>' [formmail only] + +// FEEDBACK[4] = ''; // window width +// FEEDBACK[5] = ''; // window height + +// FEEDBACK[6] = ''; // 'Send a message to teacher' [prompt/button text] +// FEEDBACK[7] = ''; // 'Title' +// FEEDBACK[8] = ''; // 'Teacher' +// FEEDBACK[8] = ''; // 'Message' +// FEEDBACK[10] = ''; // 'Close this window' [formmail only] +//} + +// ********** +// HP array +// ********** +HP = new Array(); +for (var i=0; i<=7; i++) { + HP[i] = new Array(); +} +// indexes for the HP array (makes the code further down easier to read) +_score = 0; +_weight = 1; +_correct = 2; +_wrong = 3; +_unused = 4; +_hints = 5; +_clues = 6; +_checks = 7; // ************* // Server Fields @@ -498,7 +551,7 @@ if (window.ServerFields==null) { // ********************* function QuizLogin(LoginPrompt) { - if ((Login[0] || Login[1] || Login[2] || Login[3]) && !is_LMS()) { + if (!is_LMS() && (Login[0] || Login[1] || Login[2] || Login[3])) { var html = '' + '' + '' @@ -566,7 +619,7 @@ function QuizLogin(LoginPrompt) { + '' ; for(var i=0; i' @@ -905,7 +984,7 @@ function AddStudentDetailsToResultForm() { } } if (Login[3]) { // quiz password - sDetails += makeHiddenField('Password', window.Password); + sDetails += hpHiddenField('Password', window.Password); ResultForm = replaceLast('', sDetails + '', ResultForm); } } @@ -936,10 +1015,10 @@ function AddDatabaseDetailsToResultForm() { var ext = (DB[3] ? DB[3] : 'txt'); if (ext.charAt(0)!='.') ext = '.' + ext; - dbDetails += makeHiddenField('append_db', folder + file + ext); - dbDetails += makeHiddenField('db_fields', DB[4]); - dbDetails += makeHiddenField('db_delimiter', ''); // NS6+ requires this be set later - if (DB[6]) dbDetails += makeHiddenField('env_report', DB[6]); + dbDetails += hpHiddenField('append_db', folder + file + ext); + dbDetails += hpHiddenField('db_fields', DB[4]); + dbDetails += hpHiddenField('db_delimiter', ''); // NS6+ requires this be set later + if (DB[6]) dbDetails += hpHiddenField('env_report', DB[6]); // insert dbDetails before the final tag in the ResultForm ResultForm = replaceLast('', dbDetails + '', ResultForm); @@ -957,7 +1036,7 @@ function AddServerFieldsToResultForm() { } } if (ServerFields[i][1]) { - s += makeHiddenField(ServerFields[i][0], ServerFields[i][1]); + s += hpHiddenField(ServerFields[i][0], ServerFields[i][1]); } } // end for if (s) ResultForm = replaceLast('', s + '', ResultForm); @@ -975,23 +1054,24 @@ function replaceLast(a, b, c) { // ************************* function GetQuestionDetails() { - var t = get_quiz_type(); - var v = get_quiz_version(); + var hp = hpVersion(); + var t = hpQuizType(); + var v = hpQuizVersion(); - return (t==1) ? GetJbcQuestionDetails(v) : - (t==2) ? GetJClozeQuestionDetails(v) : - (t==3) ? GetJCrossQuestionDetails(v) : - (t==4) ? GetJMatchQuestionDetails(v) : - (t==5) ? GetJMixQuestionDetails(v) : - (t==6) ? GetJQuizQuestionDetails(v) : - (t==7) ? GetRhubarbDetails(v) : - (t==8) ? GetSequiturDetails(v) : ''; + return (t==1) ? GetJbcQuestionDetails(hp, v) : + (t==2) ? GetJClozeQuestionDetails(hp, v) : + (t==3) ? GetJCrossQuestionDetails(hp, v) : + (t==4) ? GetJMatchQuestionDetails(hp, v) : + (t==5) ? GetJMixQuestionDetails(hp, v) : + (t==6) ? GetJQuizQuestionDetails(hp, v) : + (t==7) ? GetRhubarbDetails(hp, v) : + (t==8) ? GetSequiturDetails(hp, v) : ''; } -function GetJbcQuestionDetails(v) { +function GetJbcQuestionDetails(hp, v) { qDetails = ''; // check the quiz version - if (v==5 || v==6) { + if (hp==5 || hp==6) { // get question details for(var q=0; q0)) { // right - qDetails += makeHiddenField(Q+'right', aDetails[0]); + qDetails += hpHiddenField(Q+'right', aDetails[0]); } if (JBC[4] && (DB[0] || aDetails[1].length>0)) { // wrong - qDetails += makeHiddenField(Q+'wrong', aDetails[1]); + qDetails += hpHiddenField(Q+'wrong', aDetails[1]); } if (JBC[5] && (DB[0] || aDetails[2].length>0)) { // ignored - qDetails += makeHiddenField(Q+'ignored', aDetails[2]); + qDetails += hpHiddenField(Q+'ignored', aDetails[2]); } // calculate score for this question, if required (for HP version < 5.5) if (isNaN(Status[q][3])) { @@ -1036,21 +1116,22 @@ function GetJbcQuestionDetails(v) { Status[q][3] = (a1<1 || a1<(a2-1)) ? 0 : ((a1 - (a2-1)) / a1); } // add 'score' for this question - qDetails += makeHiddenField(Q+'score', Math.floor(Status[q][3]*100)+'%'); + qDetails += hpHiddenField(Q+'score', Math.floor(Status[q][3]*100)+'%'); } // end for } return qDetails; } -function GetJClozeQuestionDetails(v) { +function GetJClozeQuestionDetails(hp, v) { var qDetails = ''; // check the quiz version - if (v==5 || v==6) { + if (hp==5 || hp==6) { - var hp5 = (State[0].Guesses==null); + var r = hpRottmeier(); // get details for each question - for (var q=0; q0) qDetails += makeHiddenField(Q+'ignored', ignored); + if (DB[0] || ignored.length>0) qDetails += hpHiddenField(Q+'ignored', ignored); } - if (JCloze[3] && State[q].Guesses) { - var wrong = new Array(); - for (var i=0, ii=0; i0) qDetails += hpHiddenField(Q+'wrong', wrong); } - if (DB[0] || ii>0) qDetails += makeHiddenField(Q+'wrong', wrong); } + + var HintsAndChecks = (hp==5) ? State[q][1] : (r==0) ? State[q].HintsAndChecks : (r==1) ? GapList[q][1].NumOfTrials : (r==2) ? GapList[q][1].HintsAndChecks : 0; + var Hints = (HP[_hints][q] ? HP[_hints][q] : 0); + if (JCloze[4]) { // number of penalties - var x = (hp5 ? State[q][1] : State[q].HintsAndChecks); - qDetails += makeHiddenField(Q+'penalties', x); + qDetails += hpHiddenField(Q+'penalties', HintsAndChecks); } if (JCloze[5]) { // clue shown? - var x = (hp5 ? State[q][0] : State[q].ClueGiven); - qDetails += makeHiddenField(Q+'clue_shown', (x ? 'HOTPOT_YES' : 'HOTPOT_NO')); + var x = (hp==5) ? State[q][0] : (r==0) ? State[q].ClueGiven: (r==1) ? GapList[q][1].ClueAskedFor : false; + qDetails += hpHiddenField(Q+'clue_shown', (x ? 'YES' : 'NO')); } if (JCloze[6]) { // clue text - qDetails += makeHiddenField(Q+'clue_text', I[q][2]); + qDetails += hpHiddenField(Q+'clue_text', I[q][2]); + } + if (JCloze[7]) { // number of hints + qDetails += hpHiddenField(Q+'hints', Hints); + } + if (JCloze[8]) { // number of clues + x = HP[_clues][q] ? HP[_clues][q] : 0; + qDetails += hpHiddenField(Q+'clues', x); + } + if (JCloze[9]) { // number of checks (including the final one for the correct answer) + qDetails += hpHiddenField(Q+'checks', HintsAndChecks - Hints + (is_correct ? 1 : 0)); } } // end for } return qDetails; } -function GetJCrossQuestionDetails(v) { +function GetJCrossQuestionDetails(hp, v) { var qDetails = ''; // check the quiz version - if (v==5 || v==6) { + if (hp==5 || hp==6) { // inialize letter count var letters = 0; @@ -1116,13 +1238,13 @@ function GetJCrossQuestionDetails(v) { if (L[row][col]) letters++; // show answers and clues, if required - var q = (v==5) ? C[row][col] : CL[row][col]; + var q = (hp==5) ? C[row][col] : CL[row][col]; if (q) { // format 'Q' (a padded, two-digit version of 'q') var Q = getQ('JCross', q); - var clue_A = (v==5) ? A[q] : GetJCrossClue('Clue_A_' + q); - var clue_D = (v==5) ? D[q] : GetJCrossClue('Clue_D_' + q); + var clue_A = (hp==5) ? A[q] : GetJCrossClue('Clue_A_' + q); + var clue_D = (hp==5) ? D[q] : GetJCrossClue('Clue_D_' + q); // add separator, if required if (JCross[0] && (clue_A || clue_D)) { @@ -1130,22 +1252,48 @@ function GetJCrossQuestionDetails(v) { } if (clue_A) { // across question - if (JCross[3]) qDetails += makeHiddenField(Q+'across', GetJCrossWord(G, row, col)); - if (JCross[4]) qDetails += makeHiddenField(Q+'across_clue', clue_A); + if (JCross[3]) qDetails += hpHiddenField(Q+'across', GetJCrossWord(G, row, col)); + if (JCross[4]) qDetails += hpHiddenField(Q+'across_clue', clue_A); + if (JCross[5]) { + var x = (HP[_wrong]['A'] && HP[_wrong]['A'][q]) ? HP[_wrong]['A'][q] : ''; + qDetails += hpHiddenField(Q+'across_wrong', x); + } + if (JCross[6]) { + var x = HP[_clues][q] ? HP[_clues][q] : 0; + qDetails += hpHiddenField(Q+'across_clues', x); + } + if (JCross[7]) { + var x = (HP[_hints]['A'] && HP[_hints]['A'][q]) ? HP[_hints]['A'][q] : 0; + qDetails += hpHiddenField(Q+'across_hints', x); + } + if (JCross[8]) { + var x = (HP[_checks]['A'] && HP[_checks]['A'][q]) ? HP[_checks]['A'][q] : ''; + qDetails += hpHiddenField(Q+'across_checks', x); + } } if (clue_D) { // down question - if (JCross[3]) qDetails += makeHiddenField(Q+'down', GetJCrossWord(G, row, col, true)); - if (JCross[4]) qDetails += makeHiddenField(Q+'down_clue', clue_D); + if (JCross[3]) qDetails += hpHiddenField(Q+'down', GetJCrossWord(G, row, col, true)); + if (JCross[4]) qDetails += hpHiddenField(Q+'down_clue', clue_D); + if (JCross[5]) qDetails += hpHiddenField(Q+'down_wrong', ''); + if (JCross[6]) { + var x = HP[_clues][q] ? HP[_clues][q] : 0; + qDetails += hpHiddenField(Q+'across_clues', x); + } + if (JCross[7]) { + qDetails += hpHiddenField(Q+'down_hints', ''); + var hints = (HP[_hints]['D'] && HP[_hints]['D'][q]) ? HP[_hints]['D'][q] : 0; + } + if (JCross[8]) qDetails += hpHiddenField(Q+'down_checks', ''); } } // end if q } // end for col } // end for row if (JCross[2]) { // show number of letters - qDetails = makeHiddenField('JCross_letters', letters) + qDetails; + qDetails = hpHiddenField('JCross_letters', letters) + qDetails; } if (JCross[1]) { // show penalties - qDetails = makeHiddenField('JCross_penalties', window.Penalties) + qDetails; + qDetails = hpHiddenField('JCross_penalties', window.Penalties) + qDetails; } } @@ -1168,92 +1316,80 @@ function GetJCrossWord(a, r, c, goDown) { } return s; } -function GetJMatchQuestionDetails(v) { - var qDetails = ''; - // HP5.5 uses "I" for v5 and v6 JMatch quizzes - var hp5 = (window.I) ? true : false; - - // check the quiz version - if (hp5 || v==6 || v==6.1) { - - if (JMatch[1] && v==6.1) { // attempts - qDetails += makeHiddenField('JMatch_attempts', Penalties+1); - } - - // get number of questions - var max_q = (hp5 || v==6) ? Status.length : F.length; - - // get details for each question - for (var q=0; q=i_max) i = 0; // shouldn't happen + } else { + // get current guess, if any + var i = obj.selectedIndex; + } + if (i) rhs = obj.options[i].innerHTML; + } else { // correct + rhs = GetJMatchText(q, 'RightItem'); + } } - return (i=G) break; - } - var isWrong = (a>=A); + var q = 0; // question number // format 'Q' (a padded, two-digit version of 'q') - var Q = getQ('JMix', 0); + var Q = getQ('JMix', q); // add separator, if required if (JMix[0]) qDetails += makeSeparator(Q); // add 'score' for this question - var score = isWrong ? 0 : ((Segments.length-Penalties)/Segments.length); - qDetails += makeHiddenField(Q+'score', Math.floor(score*100)+'%'); + var score = HP[_correct]==null ? 0 : ((Segments.length-Penalties)/Segments.length); + qDetails += hpHiddenField(Q+'score', Math.floor(score*100)+'%'); if (JMix[1]) { // number of wrong guesses - qDetails += makeHiddenField(Q+'wrongGuesses', Penalties); + qDetails += hpHiddenField(Q+'wrongGuesses', Penalties); } if (JMix[2]) { // right answer - qDetails += makeHiddenField(Q+'right', GetJMixSequence(Answers[isWrong ? 0 : a])); + var x = (HP[_correct][q]) ? HP[_correct][q] : ''; + qDetails += hpHiddenField(Q+'correct', x); } - if (JMix[3] && isWrong) { // wrong answer - qDetails += makeHiddenField(Q+'wrong', GetJMixSequence(GuessSequence)); + if (JMix[3]) { // wrong answer(s) + var x = (HP[_wrong][q]) ? HP[_wrong][q] : ''; + qDetails += hpHiddenField(Q+'wrong', x); + } + if (JMix[5]) { // checks + var x = (HP[_checks][q]) ? HP[_checks][q] : 0; + qDetails += hpHiddenField(Q+'checks', x); + } + if (JMix[6]) { // hints + var x = (HP[_hints][q]) ? HP[_hints][q] : 0; + qDetails += hpHiddenField(Q+'hints', x); } } return qDetails; @@ -1272,14 +1408,14 @@ function GetJMixSegmentText(index){ } return (i=i_max) { + HP[_wrong][AD][q][i] = guess; + check = true; + } + } + + // update HP[_checks], if necessary + if (check) { + if (!HP[_checks][AD]) HP[_checks][AD] = new Array(); + if (!HP[_checks][AD][q]) HP[_checks][AD][q] = 0; + HP[_checks][AD][q]++; + } + } +} +function hpClickEnter(hp, t, v, args) { + if (t==3) { // JCross + var q = args[0]; // clue/question number + if (!HP[_enter][q]) HP[_enter][q] = 0; + HP[_enter][q]++; + } + return true; +} +function GetJMatchQuestionDetails(hp, v) { + var qDetails = ''; + + // HP5.5 uses "I" for v5 and v6 JMatch quizzes + // var hp5 = (window.I) ? true : false; + + // check the quiz version + if (hp==5 || hp==6) { + + if (JMatch[1] && v==6.1) { // attempts + qDetails += hpHiddenField('JMatch_attempts', Penalties+1); + } + + // get number of questions + var max_q = (hp==5 || v==6) ? Status.length : F.length; + + // get details for each question + for (var q=0; q=0 && value.indexOf('>')>=0) { value = ''; } @@ -1527,11 +2063,12 @@ function encode_entities(s_in) { var c = s_in.charCodeAt(i); // 34 : double quote .......["] & // 38 : single quote .......['] ' + // 43 : plus sign ..........[+] // 44 : comma ..............[,] // 60 : left angle bracket .[<] < // 62 : right angle bracket [>] > // >=128 multibyte character - s_out += (c<128) ? s_in.charAt(i) : ('&#x' + pad(c.toString(16), 4) + ';'); + s_out += (c==43 || c==44 || c>=128) ? ('&#x' + pad(c.toString(16), 4) + ';') : s_in.charAt(i); } return s_out; } @@ -1568,15 +2105,15 @@ function getTime(obj) { return s; } -function getFunction(fn) { +function getFunc(fn) { if (typeof(fn)=='string') { fn = eval('window.' + fn); } return (typeof(fn)=='function') ? fn : null; } -function getFunctionCode(fn, extra) { +function getFuncCode(fn, extraCode, anchorCode, beforeAnchor) { var s = ''; - var obj = getFunction(fn); + var obj = getFunc(fn); if (obj) { s = obj.toString(); var i1 = s.indexOf('{')+1; @@ -1585,27 +2122,63 @@ function getFunctionCode(fn, extra) { s = s.substring(i1, i2); } } - return s + (extra ? extra : ''); -} -function getFunctionArgs(fn) { - var a = new Array(); - var obj = getFunction(fn); - if (obj) { - var s = obj.toString(); - var i1 = s.indexOf('(')+1; - var i2 = s.indexOf(')'); - if (i1>0 && i10 && i1i2) i3 = i2; + a[i++] = trim(s.substring(i1, i3)); + i1 = i3+1; + } + } + + return flag ? a : getArgsStr(a); } function getPrompt(fn) { // the LoginPrompt is the text string in the first prompt(...) statement // v5 : in the StartUp function // v6 : in the GetUserName function // Note: netscape uses double-quote as delimiter, others use single quote - var s = getFunctionCode(fn); + var s = getFuncCode(fn); var i1 = s.indexOf('prompt') + 8; var i2 = s.indexOf(s.charAt(i1-1), i1); @@ -1629,7 +2202,7 @@ function getStartUpCode(fn) { // and the code after the 2nd subsequent '}' // v6 : the code before and after 'GetUserName();' // i.e. all the code except the call to 'GetUserName();' - var s = getFunctionCode(fn); + var s = getFuncCode(fn); var i1 = s.indexOf('GetUserName();'); if (i1>=0) { // v6 var i2 = i1 + 14; @@ -1640,7 +2213,308 @@ function getStartUpCode(fn) { return (0' + + '' + + '' + + '' + + '' + + '' + + '
' + FEEDBACK[7] + ':' + document.title + '
' + FEEDBACK[8] + ': ' + ; + if (typeof(FEEDBACK[1])=='string') { + html += FEEDBACK[1] + hpHiddenField('recipient', FEEDBACK[1], ',', true); + + } else if (typeof(FEEDBACK[1])=='object') { + + var i_max = FEEDBACK[1].length; + if (i_max==1) { // one teacher + html += FEEDBACK[1][0][0] + hpHiddenField('recipient', FEEDBACK[1][0][0]+' <'+FEEDBACK[1][0][1]+'>', ',', true); + + } else if (i_max>1) { // several teachers + html += ''; + } + } + html += '
' + FEEDBACK[9] + ':
 ' + + hpHiddenField('realname', FEEDBACK[2], ',', true) + + hpHiddenField('email', FEEDBACK[3], ',', true) + + hpHiddenField('subject', document.title, ',', true) + + hpHiddenField('title', document.title, ',', true) + + hpHiddenField('return_link_title', FEEDBACK[10], ',', true) + + hpHiddenField('return_link_url', 'javascript:self.close()', ',', true) + + '
' + ; + } else if (FEEDBACK[1]) { // url only + + if (typeof(FEEDBACK[1])=='object') { + var i_max = FEEDBACK[1].length; + if (i_max>1) { // several teachers + html += '' + + '
' + + '' + + '' + + '' + + '
' + FEEDBACK[7] + ':' + document.title + '
' + FEEDBACK[8] + ': ' + ; + html += ''; + html += '
 ' + + '
' + ; + } else if (i_max==1) { // one teacher + url = FEEDBACK[0] + FEEDBACK[1][0][1]; + } + + } else if (typeof(FEEDBACK[1])=='string') { + url = FEEDBACK[0] + FEEDBACK[1]; + } + } else { + url = FEEDBACK[0]; + } + if (url || html) { + var w = openWindow(url, 'feedback', FEEDBACK[4], FEEDBACK[5], 'RESIZABLE,SCROLLBARS', html); + if (!w) { + // unable to open popup window + alert(MSG[18]); + } + } + } +} + +// ******************** +// intercept clicks +// ******************** +function hpInterceptFeedback() { + // modify the function which writes feedback + // v6: ShowMessage(Feedback) + // v5: WriteFeedback(Feedback) + // v4: WriteFeedback(Stuff) + // v3: WriteFeedback(Feedback) [except JMatch] + // v3: CheckAnswer() [JMatch only] + var f = window.ShowMessage ? 'ShowMessage' : window.WriteFeedback ? 'WriteFeedback' : 'CheckAnswer'; + var s = getFuncCode(f) + 'Finish();'; + var a = getFuncArgs(f, true); + if (a[0] && window.FEEDBACK && FEEDBACK[0]) { + s = a[0] + "+='

" + '' + FEEDBACK[6] + "';" + s; + } + eval('window.' + f + '=new Function(' + getArgsStr(a) + 's);'); +} +function hpInterceptHints() { + // modify the function which shows hints + // JBC: none + // JCloze v3-v6: ShowHint() + // JCross v3: Cheat(), v4: ShowHint(), v5-v6[HP5]: ShowHint(Across,x,y,BoxName), v6[HP6]: ShowHint(Across,ClueNum,x,y,BoxId) + // JMatch: none + // JMix v5-v6: CheckAnswer(CheckType=1) + // JQuiz v3: CheckAnswer(ShowHint=true, QNum), v4: CheckAnswer(ShowHint=true), v5-v6[HP5]: CheckAnswer(ShowHint=true,QNum), v6[HP6]: ShowHint(QNum) + + var x = ''; // extra code, if any + + if (window.Cheat) { + // JCross v3 ? + + } else if (window.ShowHint) { + var f = 'ShowHint'; + var a = getFuncArgs(f, true); + + if (a.length==0) { + if (window.FindCurrent) { + // JCloze v3-v6 + x = 'var q=window.Locked?-1:FindCurrent();if(q>=0&&GetHint(q))hpClick(1,q);'; + } else { + // JCross v4 + // work out which box would have a hint added + // work out which question that box is part of using GridMap and WinLetters + } + + } else if (a[0]=='Across') { + if (a[1]=='ClueNum') { + // JCross v6 [HP6] + x = "var args=new Array(ClueNum,Across?'A':'D');hpClick(1,args);"; + } else if (a[1]=='x' && a[2]=='y') { + // JCross v5-v6 [HP5] + x = "var args=new Array(C[x][y],Across?'A':'D');hpClick(1,args);"; + } + + } else if (a[0]=='QNum') { + // JQuiz v6[HP6] + x = 'hpClick(1,QNum);'; + } + + } else if (window.CheckAnswer) { + var f = 'CheckAnswer'; + var a = getFuncArgs(f, true); + + if (a[0]=='ShowHint') { + if (a[1]=='QNum') { + // JQuiz v3, v5-v6[HP5] + x = 'if(ShowHint)hpClick(1,QNum);'; + } else { + // JQuiz v4 + x = 'if(ShowHint)hpClick(1,QNum-1);'; // QNum is a global variable + } + } else if (a[0]=='CheckType') { + // JMix v5-v6 + x = 'if(CheckType==1)hpClick(1,0);'; // question number is always 0; + } + } + // add the e(x)tra code, if any, to the start of the hint (f)unction + if (x) { + var s = getFuncCode(f, x, '', true); + eval('window.' + f + '=new Function(' + getArgsStr(a) + 's);'); + } +} +function hpInterceptClues() { + // modify the function which shows clues (or ShowAnswers in JQuiz) + // JBC: none + // JCloze v3-v6: ShowClue(ItemNum) + // JCross v3-v4: ShowClue(ClueNum), v5-v6: ShowClue(ClueNum,x,y) + // JMatch none + // JMix none + // JQuiz ShowAnswers(QNum) + + var x = ''; // extra code, if any + + if (window.ShowClue) { + var f = 'ShowClue'; + var a = getFuncArgs(f, true); + + if (a[0]=='ItemNum') { + // JCloze (v3-v6) + x = 'if(!window.Locked)hpClick(2,ItemNum);'; // v6 [HP6] uses window.Locked + + } else if (a[0]=='ClueNum') { + if (a[1]=='x' && a[2]=='y') { + if (window.A && window.D) { + // JCross v5-v6 [HP5] + x = 'if(A[ClueNum]||D[ClueNum])hpClick(2,ClueNum);'; + } else if (document.getElementById) { + // JCross v6 [HP6] + x = "if(document.getElementById('Clue_A_' + ClueNum)||document.getElementById('Clue_D_' + ClueNum))hpClick(2,ClueNum);"; + } + } else { + if (window.AClues && window.DClues) { + // JCross v3-v4 + x = 'if(AClues[ClueNum]||DClues[ClueNum])hpClick(2,ClueNum);'; + } + } + } + } + // JQuiz: there is no "ShowClue" function but there is a "ShowAnswer" function + if (window.ShowAnswers) { + var f = 'ShowAnswers'; + var a = getFuncArgs(f, true); + if (window.State) { + if (window.ShowMessage) { + // JQuiz v6 [HP6] + x = 'if(State[QNum][0]<1)hpClick(2,QNum);'; + } else if (window.WriteFeedback) { + // JQuiz v3-v4 + x = 'if(State[QNum-1][0]<1)hpClick(2,QNum-1);'; + } + } else if (window.Status) { + // JQuiz v5-v6 [HP5] + x = 'if(Status[QNum][0]<0)hpClick(5,QNum);'; + } + + } + // add the e(x)tra code, if any, to the start of the clue (f)unction + if (x) { + var s = getFuncCode(f, x, '', true); + eval('window.' + f + '=new Function(' + getArgsStr(a) + 's);'); + } +} +function hpInterceptChecks() { + // modify the function which handles checks + // JBC: none + // JCloze none + // JCross none + // JMatch HP5 v3, v5, v6: CheckAnswer(), HP5 v4: CheckResults(), HP6: CheckAnswers() + // JMix CheckAnswer(CheckType) + // JQuiz + // HP5: CheckAnswer(ShowHint, QNum) + // HP6: CheckMCAnswer, CheckMultiSelAnswer, CheckShortAnswer + // Sequitur CheckAnswer(Chosen, Btn) + + // HP6 JQuiz has three "Check Answer" functions + var f = new Array('CheckMCAnswer', 'CheckMultiSelAnswer', 'CheckShortAnswer'); + for (var i=0; i=0 && window.ShowMessage) { //javascript:var s="";var x=document.forms.QForm.elements;for(var i=0;i=0) ? 3 : (f.QForm && self.RItems) ? 4 : (f.ButtonForm) ? 5 : (f.QForm0 && f.Buttons0) ? 6 : 0; + t = (f.QForm && f.QForm.elements[0].name.substring(0,3)=='FB_') ? 1 : (f.Cloze) ? 2 : (w.GetAnswerOpener && GetAnswerOpener.indexOf('AnswerForm')>=0) ? 3 : (f.QForm && w.RItems) ? 4 : (f.ButtonForm) ? 5 : (f.QForm0 && f.Buttons0) ? 6 : 0; - } else if (GetObj(d, 'MainDiv')) { + } else if (hpObj(d, 'MainDiv')) { v = 6; var obj = (f.QForm) ? f.QForm.elements : null; - t = (obj && obj.length>0 && obj[0].id=='') ? 1 : (f.Cloze) ? 2 : (GetObj(d, 'GridDiv') || GetObj(d, 'Clues')) ? 3 : GetObj(d, 'MatchDiv') ? 4 : GetObj(d, 'SegmentDiv') ? 5 : ((f.QForm && f.QForm.Guess) || GetObj(d, 'Questions')) ? 6 : 0; + t = (obj && obj.length>0 && obj[0].id=='') ? 1 : (f.Cloze) ? 2 : (hpObj(d, 'GridDiv') || hpObj(d, 'Clues')) ? 3 : hpObj(d, 'MatchDiv') ? 4 : hpObj(d, 'SegmentDiv') ? 5 : ((f.QForm && f.QForm.Guess) || hpObj(d, 'Questions')) ? 6 : 0; + + // sniff Rottmeier quizzes + if (window.Create_StateArray) { + if (t==2) { // JCloze + var obj = new Create_StateArray(); + if (typeof(obj.GapLocked)=='boolean') r = 1; // drop-down (v2.4) + else if (typeof(obj.ErrorFound)=='boolean') r = 2; // find-it (v3.1a + v3.1b) + obj = null; // prevents memory leakage on some versions of IE + } + } - } else if (GetObj(d, 'D0')) { + } else if (hpObj(d, 'D0')) { v = 6.1; // drag and drop (HP5 and HP6) - t = (GetObj(d, 'F0')) ? 4 : (GetObj(d, 'Drop0')) ? 5 : 0; + t = (hpObj(d, 'F0')) ? 4 : (hpObj(d, 'Drop0')) ? 5 : 0; - } else if (window.Words && f.Rhubarb) { + } else if (w.Words && f.Rhubarb) { v = 6; t = 7; // rhubarb (TexToys) - } else if (window.Segments && GetObj(d, 'Story')) { + } else if (w.Segments && hpObj(d, 'Story')) { v = 6; t = 8; // sequitur (TexToys) } - - if (v) quiz.v = v; // intended browser version - if (t) quiz.t = t; // quiz type + quiz.v = v; // intended browser version + quiz.t = t; // quiz type + quiz.r = r; // rottmeier quiz type } } -function get_quiz_type() { - sniff_quiz(); +function hpRottmeier() { + hpDetectQuiz(); + return quiz.r; +} +function hpVersion() { + hpDetectQuiz(); + return quiz.hp; +} +function hpQuizType() { + hpDetectQuiz(); return quiz.t; } -function get_quiz_version() { - sniff_quiz(); +function hpQuizVersion() { + hpDetectQuiz(); return quiz.v; } +function hpScoreEngine(score_i, a, s, aa, ss, count_c, count_i) { + // calculate the score for the quiz so far -function all_finished(a, s, aa, ss) { + // score_i : amount by which to increment "score" + // a : outer array + // s : condition, if any, on outer array (=a) + // if true, the score will be incremented by "score_i" + // aa : inner array, if any + // ss : condition, if any, on inner array (=aa) + // count_c : condition, if any, on which "count" is to be incremented + // count_i : amount by which to increment "count" + + // "a" and "aa" may be passed as arrays or strings containing the name of an array + // "s" and "ss" are strings containing an expression to be eval(uated) + // "score_i", "count_i" and "count_c" strings containing an expression to be eval(uated) + + var score = 0; + var count = 0; + + // set default condition to increment "count", and amount by which to increment the count + if (count_c==null) count_c = "true"; + if (count_i==null) count_i = "1"; + + // set length of outer array. if any + var l = (typeof(a)=="string") ? eval(a + ".length") : a ? a.length : 0; + + // loop through outer array + for (var i=0; i0 && a[i]=='0'"); // doesn't work + else if (v==4) x = hpScoreEngine(1, DoneStatus, "a[i]==0"); // doesn't work + else if (v==5 || v==6) x = hpScoreEngine("a[i][3]", Status, "a[i][3]"); + + + } else if (t==2) { // jcloze + + if (v==3 || v==4) x = hpScoreEngine("a[i]", Scores); + else if (hp==5) x = hpScoreEngine("a[i][3]", State); // v==5 && v==6 + else if (hp==6) { + var r = hpRottmeier(); + if (r) x = hpScoreEngine("a[i][1].Score", GapList); + else x = hpScoreEngine("a[i].ItemScore", State); + } + + } else if (t==3) { // jcross + + if (v==3) x = hpScoreEngine(1, CorrectAnswers, "document.QuizForm.elements[i*2].selectedIndex==a[i]"); + else if (v==4) x = hpScoreEngine(1, WinLetters, "ConvertCase(GetBoxValue(i),1).charAt(0)==a[i].charAt(0)"); + else if (v==5 || v==6) x = hpScoreEngine(1, L, "", "L[i]", "L[i][ii] && L[i][ii]==G[i][ii]", "L[i][ii]"); + + } else if (t==4) { // jmatch + + if (v==3) x = hpScoreEngine(1, CorrectAnswers, "document.QuizForm.elements[i*2].selectedIndex==a[i]"); + else if (v==4) x = hpScoreEngine(1, Draggables, "a[i].correct=='1'"); + else if (v==5) x = hpScoreEngine(1, I, "I[i][2]<1 && I[i][0].length>0 && Status[i][0]==1"); + else if (v==6) x = hpScoreEngine("Math.min(score,(Status.length-Status[i][1])/Status.length)-score", Status, "true"); + else if (v==5.1 || v==6.1) x = hpScoreEngine(1, D, "D[i][2]==D[i][1] && D[i][2]>0"); + + } else if (t==5) { // jmix + + // there was no v3 or v4 of JMix + if (v==5 || v==6 || v==6.1) x = Math.floor(100*(Segments.length-Penalties)/Segments.length); + + } else if (t==6) { // jquiz + + if (hp==5) { + if (v==3 || v==4) x = hpScoreEngine("a[i][4]/10", State, "a[i][0]==1"); + else if (v==5 || v==6) x = hpScoreEngine("a[i][4]/10", Status, "a[i][0]==1", "", "", "true", "1"); + + } else if (hp==6) { + if (v==6) x = hpScoreEngine("I[i][0]*a[i][0]", State, "a[i]&&a[i][0]>=0", "", "", "a[i]", "I[i][0]"); + } + + } else if (t==7) { // rhubarb + if (v==6) { + x = hpScoreEngine(1, DoneList, "a[i]==1"); + } + + } else if (t==8) { // sequitur + if (v==6) x = Math.floor(100*ScoredPoints/TotalPoints); + } + + return x; // result +} + +function hpFinishedEngine(a, s, aa, ss) { // determine whether or not all quistions in a quiz are finished // a : outer array // s : condition, if any, on outer array + // if true for any element in "a", the quiz is NOT finished // aa : inner array, if any // ss : condition, if any, on inner array + // if true for any element in "aa", the quiz is NOT finished // the arrays "a" and "aa" may be passed as arrays or strings to be eval(uated) // the conditions "s" and "ss" are specified as strings to be eval(uated) // assume a positive result - var r = true; + var x = true; // set length of outer array. if any var l = (typeof(a)=="string") ? eval(a + ".length") : a ? a.length : 0; @@ -1846,73 +2890,87 @@ function all_finished(a, s, aa, ss) { for (var i=0; i0 && a[i]=='0'"); - else if (v==4) r = all_finished(DoneStatus, "a[i]==0"); - else if (v==5 || v==6) r = all_finished(Status, "a[i][0]==0"); + if (v==3) x = hpFinishedEngine(DoneStatus, "i>0 && a[i]=='0'"); + else if (v==4) x = hpFinishedEngine(DoneStatus, "a[i]==0"); + else if (v==5 || v==6) x = hpFinishedEngine(Status, "a[i][0]==0"); } else if (t==2) { // jcloze - if (v==3 || v==4 || v==5 || v==6) r = all_finished(I, "CheckAnswer(i)==-1"); - // also: else if (v==5 || v==6) r = all_finished(State, "a[i][4]!=1") + var r = hpRottmeier(); + if (r==1) x = hpFinishedEngine(GapList, "a[i][1].GapLocked==false"); // drop-down + else if (r==2) x = hpFinishedEngine(GapList, "a[i][1].ErrorFound==false"); // find-it + else if (v==3 || v==4 || v==5 || v==6) x = hpFinishedEngine(I, "CheckAnswer(i)==-1"); + // also: else if (v==5 || v==6) x = hpFinishedEngine(State, "a[i][4]!=1") } else if (t==3) { // jcross - if (v==3) r = all_finished(document.Crossword.elements, "ConvertCase(is.mac?unescape(MacStringToWin(a[i].value)):a[i].value,1)!=Letters[i]"); - else if (v==4) r = all_finished(WinLetters, "ConvertCase(GetBoxValue(i),1).charAt(0) != a[i].charAt(0)"); - else if (v==5) r = all_finished(L, "", "L[i]", "L[i][ii] && L[i][ii]!=G[i][ii]"); + if (v==3) x = hpFinishedEngine(document.Crossword.elements, "ConvertCase(is.mac?unescape(MacStringToWin(a[i].value)):a[i].value,1)!=Letters[i]"); + else if (v==4) x = hpFinishedEngine(WinLetters, "ConvertCase(GetBoxValue(i),1).charAt(0) != a[i].charAt(0)"); + else if (v==5 || v==6) x = hpFinishedEngine(L, "", "L[i]", "L[i][ii] && L[i][ii]!=G[i][ii]"); } else if (t==4) { // jmatch - if (v==3) r = all_finished(CorrectAnswers, "document.QuizForm.elements[i*2].selectedIndex != a[i]"); - else if (v==4) r = all_finished(Draggables, "a[i].correct!='1'"); - else if (v==5) r = all_finished(I, "I[i][2]<1 && I[i][0].length>0 && Status[i][0]<1 && GetAnswer(i)!=I[i][3]"); - else if (v==6) r = all_finished(D, "D[i][2]==0 || D[i][2]!=D[i][1]"); + if (v==3) x = hpFinishedEngine(CorrectAnswers, "document.QuizForm.elements[i*2].selectedIndex != a[i]"); + else if (v==4) x = hpFinishedEngine(Draggables, "a[i].correct!='1'"); + else if (v==5) x = hpFinishedEngine(I, "I[i][2]<1 && I[i][0].length>0 && Status[i][0]<1 && GetAnswer(i)!=I[i][3]"); + else if (v==6) x = hpFinishedEngine(Status, "Status[i][0]<1"); + else if (v==5.1 || v==6.1) x = hpFinishedEngine(D, "D[i][2]==0 || D[i][2]!=D[i][1]"); } else if (t==5) { // jmix // there was no v3 or v4 of JMix - if (v==5 || v==6) r = !all_finished(Answers, "a[i].join(',')=='" + GuessSequence.join(',') + "'"); + if (v==5 || v==6 || v==6.1) x = !hpFinishedEngine(Answers, "a[i].join(',')=='" + GuessSequence.join(',') + "'"); } else if (t==6) { // jquiz - if (v==3 || v==4) r = all_finished(State, "a[i][0]==0"); - else if (v==5 || v==6) r = all_finished(State, "a[i] && a[i][0]<0"); + if (v==3 || v==4) x = hpFinishedEngine(State, "a[i][0]==0"); + else if (v==5 || v==6) { + if (hp==5) x = hpFinishedEngine(Status, "a[i][0]<1"); + else if (hp==6) x = hpFinishedEngine(State, "a[i] && a[i][0]<0"); + } } else if (t==7) { // rhubarb - if (v==6) r = all_finished(DoneList, "a[i]==1"); + if (v==6) x = hpFinishedEngine(DoneList, "a[i]==1"); } else if (t==8) { // sequitur - if (v==6) r = (CurrentNumber==TotalSegments || AllDone); + if (v==6) x = (CurrentNumber==TotalSegments || AllDone); } - return r; // result + return x; } -function GetObj(d, id) { +function hpObj(d, id) { return d.getElementById ? d.getElementById(id) : d.all ? d.all[id] : d[id]; } @@ -1920,28 +2978,51 @@ function GetObj(d, id) { // initialization // ************** -if (window.Finish==null) { // v3, v4 and v5 - // modify the function which writes feedback to call Finish() if the quiz is finished - // usually this is the WriteFeedback() - // but v3 of JMatch uses CheckAnswer() - var f = window.WriteFeedback ? 'WriteFeedback' : 'CheckAnswer'; - var s = getFunctionCode(f, 'if(is_finished())Finish();'); - var a = getFunctionArgs(f); - eval('window.' + f + '=new Function(' + a + 's)'); -} +hpInterceptFeedback(); +hpInterceptHints(); +hpInterceptClues(); +hpInterceptChecks(); -// the standard Finish() function -// for v6, this overwrites the original function -function Finish(){ - var f = document.store; - if (f) { - // hotpot use "Score", TexToys use "FinalScore" - var mark = (window.Score ? Score : window.FinalScore ? FinalScore : 0); - f.starttime.value = getTime(Start_Time); - f.endtime.value = getTime(); - f.mark.value = mark; - f.detail.value = ''+GetQuestionDetails()+''; - f.submit(); +function hpFindForm(name, w) { + if (w==null) w = self; + var f = w.document.forms[name]; + if (f==null && w.frames) { + for (var i=0; i'; + if (hpForm.status) { + if (!quizstatus) { + // 4=completed, 3=abandoned, 2=timed-out or 1=in-progress + quizstatus = hpFinished() ? 4 : hpTimedOut() ? 2 : 1; + } + hpForm.status.value = quizstatus; + } + if (!window.sentquizresults) { + if (hpForm.status && quizstatus==4) { + window.sentquizresults = true; + } + if (quizstatus==4) { // completed + // wait 2 seconds for student to see feedback + setTimeout("hpForm.submit();", 2000); + } else { + hpForm.submit(); + } + } + } else if (hpFinished()) { + SendAllResults(mark); } } @@ -1950,14 +3031,14 @@ if (DB[7] && DB[8] && !is_LMS()) { ResultForm = '' + '' + '
' - + makeHiddenField('recipient', '') - + makeHiddenField('subject', '') - + makeHiddenField('Exercise', '') - + makeHiddenField('realname', '') - + makeHiddenField('Score', '') - + makeHiddenField('Start_Time', '') - + makeHiddenField('End_Time', '') - + makeHiddenField('title', 'Thanks!') + + hpHiddenField('recipient', '') + + hpHiddenField('subject', '') + + hpHiddenField('Exercise', '') + + hpHiddenField('realname', '') + + hpHiddenField('Score', '') + + hpHiddenField('Start_Time', '') + + hpHiddenField('End_Time', '') + + hpHiddenField('title', 'Thanks!') + '
' + '' ; @@ -1967,7 +3048,8 @@ var p = getPrompt(window.GetUserName || window.StartUp); var c = getStartUpCode(window.StartUp); if (p && c) { window.StartUp = new Function('QuizLogin("' + p + '")'); - window.StartQuiz = new Function('if(!is_LMS()){' + c + '}'); + window.StartQuiz = new Function(c); + // "QuizLogin" finshes by calling "StartQuiz" } // reassign the SendResults function diff --git a/mod/hotpot/index.php b/mod/hotpot/index.php index 9025e98350a..8b7eb7806c9 100644 --- a/mod/hotpot/index.php +++ b/mod/hotpot/index.php @@ -3,6 +3,7 @@ // This page lists all the instances of hotpot in a particular course require_once("../../config.php"); + require_once("../../course/lib.php"); require_once("lib.php"); $id = required_param("id"); // course @@ -15,11 +16,22 @@ add_to_log($course->id, "hotpot", "view all", "index.php?id=$course->id", ""); - // Print the header + // Moodle 1.4+ requires sesskey to be passed in forms + if (isset($USER->sesskey)) { + $sesskey = ''; + } else { + $sesskey = ''; + } + // get message strings for titles $strmodulenameplural = get_string("modulenameplural", "hotpot"); $strmodulename = get_string("modulename", "hotpot"); + // string translation array for single and double quotes + $quotes = array("'"=>"\'", '"'=>'"'); + + // Print the header + $title = "$course->shortname: $strmodulenameplural"; $heading = "$course->fullname"; $navigation = "$strmodulenameplural"; @@ -30,89 +42,235 @@ $next_url = "$CFG->wwwroot/course/view.php?id=$course->id"; - // Get all instances of this module - if (! $hotpots = get_all_instances_in_course("hotpot", $course)) { - notice("There are no $strmodulenameplural", $next_url); - die; + // get display section, if any + $section = optional_param('section', 0); + if ($section) { + $displaysection = course_set_display($course->id, $section); + } else { + if (isset($USER->display[$course->id])) { + $displaysection = $USER->display[$course->id]; + } else { + $displaysection = 0; + } } + + // Get all instances of this module + if (!$hotpots = hotpot_get_all_instances_in_course("hotpot", $course)) { + $hotpots = array(); + } + - if (isadmin()) { - if (isset($_POST['regrade'])) { - $hotpotids = array(); - foreach ($hotpots as $hotpot) { - $hotpotids[] = $hotpot->id; - } - $hotpotids = implode(',', $hotpotids); - - $select = "hotpot IN ($hotpotids)"; - - $questionids = array(); - if ($questions = get_records_select("hotpot_questions", $select)) { - $questionids = array_keys($questions); - } - $questionids = implode(',', $questionids); - - if ($questionids) { - hotpot_delete_and_notify( - 'hotpot_questions', - "id IN ($questionids)", - get_string('question', 'quiz') - ); - hotpot_delete_and_notify( - 'hotpot_responses', - "question IN ($questionids)", - get_string('answer', 'quiz') - ); - } - - if ($attempts = get_records_select('hotpot_attempts', $select)) { - $count = 0; - foreach ($attempts as $attempt) { - if (isset($attempt->score)) { - hotpot_add_attempt_details($attempt); - $attempt->details = addslashes($attempt->details); - if (! update_record('hotpot_attempts', $attempt)) { - error("Could not update attempt record: ".$db->ErrorMsg(), $next_url); - } - $count++; - if ($count%10 == 0) { - print "."; - if ($count%200 == 0) { - print "
\n"; - } - hotpot_flush(300); - } - } - } - if ($count) { - notify(get_string('added', 'moodle', "$count x ".get_string('attempts', 'quiz'))); - } - notify(get_string('regradecomplete', 'quiz')); + // if necessary, remove hotpots that are not in section0 or this $USER's display section + if ($displaysection) { + foreach ($hotpots as $cmid=>$hotpot) { + if ($hotpot->section!=0 && $hotpot->section!=$displaysection) { + unset($hotpots[$cmid]); } } - print '
'; - print ''; - print ''; - print '
'."\n"; } - // Print the list of instances of this module + if (empty($hotpots)) { + notice("There are no $strmodulenameplural", $next_url); + exit; + } - $timenow = time(); - $strupdate = get_string("update"); - $strusers = get_string("users"); + // get list of hotpot ids + $hotpotids = array(); + foreach ($hotpots as $cmid=>$hotpot) { + $hotpotids[] = $hotpot->id; + } + $hotpotids = implode(',', $hotpotids); - // Moodle 1.4+ requires sesskey to be passed in forms - if (isset($USER->sesskey)) { - $sesskey = ''; + if (isadmin()) { + + // get regrade settings, if any + $regrade = optional_param("regrade"); + $confirm = optional_param("confirm"); + + // check regrade is valid + unset($regrade_cmid); + if (isset($regrade)) { + foreach ($hotpots as $cmid=>$hotpot) { + $found = false; + if ($hotpot->id==$regrade) { + $regrade_cmid = $cmid; + } + } + } + + // regrade, if necessary + if (isset($regrade_cmid)) { + + if (empty($confirm)) { + + $strregradecheck = get_string('regradecheck', 'hotpot', $hotpots[$regrade_cmid]->name); + + print_simple_box_start("center", "60%", "#FFAAAA", 20, "noticebox"); + print_heading($strregradecheck); + print '' + . '
' + . '
' + . '' + . '' + . '' + . $sesskey + . '' + . '
' + . '
  ' + . '
' + . '' + . $sesskey + . '' + . '
' + . '
' + ; + print_simple_box_end(); + print_footer($course); + exit; + + } else { // regrade has been confirmed, so proceed + + if ($regrade=='all') { + $select = "hotpot IN ($hotpotids)"; + } else { + $select = "hotpot=$regrade"; + } + + $questionids = array(); + if ($questions = get_records_select("hotpot_questions", $select)) { + $questionids = array_keys($questions); + } + $questionids = implode(',', $questionids); + + if ($questionids) { + hotpot_delete_and_notify('hotpot_questions', "id IN ($questionids)", get_string('question', 'quiz')); + hotpot_delete_and_notify('hotpot_responses', "question IN ($questionids)", get_string('answer', 'quiz')); + } + + if ($attempts = get_records_select('hotpot_attempts', $select)) { + + // start counter and timer + $count = 0; + $start = microtime(); + foreach ($attempts as $attempt) { + $attempt->details = get_field('hotpot_details', 'details', 'attempt', "$attempt->id"); + if ($attempt->details) { + hotpot_add_attempt_details($attempt); + if (! update_record('hotpot_attempts', $attempt)) { + error("Could not update attempt record: ".$db->ErrorMsg(), $next_url); + } + } + $count++; + } + if ($count) { + notify(get_string('added', 'moodle', "$count x ".get_string('attempts', 'quiz'))); + } + $msg = get_string('regradecomplete', 'quiz'); + if (!empty($CFG->hotpot_showtimes)) { + $duration = format_time(sprintf("%0.2f", microtime_diff($start, microtime()))); + $msg .= " ($duration)"; + } + notify($msg); + } + } + } // end regrade + + //print '
'; + //print ''; + //print ''; + //print '
'."\n"; + + + // get duplicate hotpot-name questions + // - JMatch LHS is longer than 255 bytes + // - JQuiz question text is longer than 255 bytes + // - other unidentified situations ?!? + + $field = ''; + $questions = false; + $regradehotpots = array(); + + switch (strtolower($CFG->dbtype)) { + case 'mysql' : + $field = "CONCAT(hotpot, '_', name)"; + break; + case 'postgres7' : + $field = "hotpot||'_'||name"; + break; + } + if ($field) { + $questions = get_records_sql(" + SELECT $field, COUNT(*), hotpot, name + FROM {$CFG->prefix}hotpot_questions + WHERE hotpot IN ($hotpotids) + GROUP BY hotpot, name + HAVING COUNT(*) >1 + "); + } + if ($questions) { + foreach ($questions as $question) { + $regradehotpots[] = $question->hotpot; + } + $regradehotpots = array_unique($regradehotpots); + sort($regradehotpots); + } + } + + // start timer + $start = microtime(); + + // get total number of attempts, users and details for these hotpots + $tables = "{$CFG->prefix}hotpot_attempts AS a"; + $fields = " + a.hotpot AS hotpot, + COUNT(DISTINCT a.clickreportid) AS attemptcount, + COUNT(DISTINCT a.userid) AS usercount, + MAX(a.score) AS maxscore + "; + $select = "a.hotpot IN ($hotpotids)"; + if (isteacher($course->id)) { + // do nothing (=get all users) } else { - $sesskey = ''; + // restrict results to this user only + $select .= " AND a.userid='$USER->id'"; } + $usejoin = 1; + if (isadmin() && $usejoin) { + // join attempts table and details table + $tables .= ",{$CFG->prefix}hotpot_details AS d"; + $fields .= ',COUNT(DISTINCT d.id) AS detailcount'; + $select .= " AND a.id=d.attempt"; + + // this may take about twice as long as getting the gradecounts separately :-( + // so this operation could be done after getting the $totals from the attempts table + } + $totals = get_records_sql("SELECT $fields FROM $tables WHERE $select GROUP BY a.hotpot"); + + if (isadmin() && empty($usejoin)) { + foreach ($hotpots as $hotpot) { + $totals[$hotpot->id]->detailcount = 0; + if ($ids = get_records('hotpot_attempts', 'hotpot', $hotpot->id)) { + $ids = join(',', array_keys($ids)); + $totals[$hotpot->id]->detailcount = count_records_select('hotpot_details', "attempt IN ($ids)"); + } + } + } + + // message strings for main table + $strusers = get_string('users'); + $strupdate = get_string('update'); + $strregrade = get_string('regrade', 'hotpot'); + $strneverclosed = get_string('neverclosed', 'hotpot'); + $strregraderequired = get_string('regraderequired', 'hotpot'); // column headings and attributes $table->head = array(); $table->align = array(); + if (!empty($CFG->hotpot_showtimes)) { + print '

'.sprintf("%0.3f", microtime_diff($start, microtime())).' secs'."

\n"; + } + switch ($course->format) { case 'weeks' : $title = get_string("week"); @@ -138,17 +296,33 @@ get_string("bestgrade", "quiz"), get_string("attempts", "quiz") ); - array_push($table->align, "left", "left", "center", "left"); + array_push($table->align, + "left", "left", "center", "left" + ); + if (isadmin()) { + array_push($table->head, $strregrade); + array_push($table->align, "center"); + } - $currentsection = ""; + $currentsection = -1; foreach ($hotpots as $hotpot) { $printsection = ""; - if ($hotpot->section !== $currentsection) { + if ($hotpot->section != $currentsection) { if ($hotpot->section) { $printsection = $hotpot->section; + if ($course->format=='weeks' || $course->format=='topics') { + // Show the zoom boxes + if ($displaysection==$hotpot->section) { + $strshowall = get_string('showall'.$course->format); + $printsection .= '

'; + } else { + $strshowone = get_string('showonly'.preg_replace('|s$|', '', $course->format, 1), '', $hotpot->section); + $printsection .= '

'; + } + } } - if ($currentsection !== "") { + if ($currentsection>=0) { $table->data[] = 'hr'; } $currentsection = $hotpot->section; @@ -156,34 +330,33 @@ $class = ($hotpot->visible) ? '' : 'class="dimmed" '; $quizname = ''.$hotpot->name.''; - $quizclose = userdate($hotpot->timeclose); + $quizclose = empty($hotpot->timeclose) ? $strneverclosed : userdate($hotpot->timeclose); - $select = isteacher($course->id) ? '' : "userid='$USER->id' AND "; - $select .= "hotpot='$hotpot->id' AND timefinish>0"; + // are there any totals for this hotpot? + if (empty($totals[$hotpot->id]->attemptcount)) { + $report = " "; + $bestscore = " "; - $attempttable = "{$CFG->prefix}hotpot_attempts"; - - // get number of attempts. if any - if ($attemptcount = count_records_sql("SELECT COUNT(*) FROM $attempttable WHERE $select")) { - - // report number of attempts (and users) - $report = get_string("viewallreports","quiz", $attemptcount); + } else { + // report number of attempts and users + $report = get_string("viewallreports","quiz", $totals[$hotpot->id]->attemptcount); if (isteacher($course->id)) { - $usercount = count_records_sql("SELECT COUNT(DISTINCT userid) FROM $attempttable WHERE $select"); - $report .= " ($usercount $strusers)"; + $report .= " (".$totals[$hotpot->id]->usercount." $strusers)"; } $report = ''.$report.''; // get best score - $bestscore = count_records_sql("SELECT MAX(score) FROM $attempttable WHERE $select"); - if (is_numeric($bestscore)) { - $bestscore .= " / $hotpot->grade"; + if (is_numeric($totals[$hotpot->id]->maxscore)) { + $bestscore = $totals[$hotpot->id]->maxscore." / $hotpot->grade"; } else { $bestscore = " "; } - } else { // no attempts - $report = " "; - $bestscore = " "; + } + + if (isadmin()) { + if (in_array($hotpot->id, $regradehotpots)) { + $report .= ' '.$strregraderequired.''; + } } $data = array (); @@ -193,16 +366,37 @@ } if (isteacheredit($course->id)) { - $update = '' - . '
' + $updatebutton = '' + . '' . '' . $sesskey . '' . '
' ; - array_push($data, $update); + array_push($data, $updatebutton); } + array_push($data, $quizname, $quizclose, $bestscore, $report); + + if (isadmin()) { + if (empty($totals[$hotpot->id]->detailcount)) { + // no details records for this hotpot, so disable regrade + $regradebutton = ' '; + } else { + $strregradecheck = get_string('regradecheck', 'hotpot', strtr($hotpot->name, $quotes)); + $regradebutton = '' + . '
' + . '' + . '' + . '' + . $sesskey + . '' + . '
' + ; + } + array_push($data, $regradebutton); + } + $table->data[] = $data; } @@ -211,20 +405,5 @@ print_table($table); // Finish the page - print_footer($course); - -/////////////////// -// functions - -function hotpot_flush($n=0, $time=false) { - if ($time) { - $ti = strftime("%X",time()); - } else { - $ti = ""; - } - echo str_repeat(" ", $n) . $ti . "\n"; - flush(); -} - - + print_footer($course); ?> diff --git a/mod/hotpot/lib.php b/mod/hotpot/lib.php index cf87f70ce4f..0eb36a49680 100644 --- a/mod/hotpot/lib.php +++ b/mod/hotpot/lib.php @@ -1,14 +1,33 @@ hotpotroot = $CFG->dirroot.DIRECTORY_SEPARATOR.'mod'.DIRECTORY_SEPARATOR.'hotpot'; +if (!isset($CFG->hotpot_showtimes)) { + set_config("hotpot_showtimes", 0); +} +if (!isset($CFG->hotpot_excelencodings)) { + set_config("hotpot_excelencodings", ""); +} + + +////////////////////////////////// +/// CONSTANTS and GLOBAL VARIABLES + +$ds = DIRECTORY_SEPARATOR; +$CFG->hotpotroot = "$CFG->dirroot{$ds}mod{$ds}hotpot"; +$CFG->hotpottemplate = "$CFG->hotpotroot{$ds}template"; define("HOTPOT_JS", "$CFG->wwwroot/mod/hotpot/hotpot-full.js"); define("HOTPOT_NO", "0"); define("HOTPOT_YES", "1"); +define ("HOTPOT_TEXTSOURCE_QUIZ", "0"); +define ("HOTPOT_TEXTSOURCE_FILENAME", "1"); +define ("HOTPOT_TEXTSOURCE_FILEPATH", "2"); +define ("HOTPOT_TEXTSOURCE_SPECIFIC", "3"); + define("HOTPOT_LOCATION_COURSEFILES", "0"); define("HOTPOT_LOCATION_SITEFILES", "1"); @@ -18,9 +37,10 @@ $HOTPOT_LOCATION = array ( ); define("HOTPOT_OUTPUTFORMAT_BEST", "1"); -define("HOTPOT_OUTPUTFORMAT_V3", "11"); -define("HOTPOT_OUTPUTFORMAT_V4", "12"); -define("HOTPOT_OUTPUTFORMAT_V5", "13"); +define("HOTPOT_OUTPUTFORMAT_V3", "10"); +define("HOTPOT_OUTPUTFORMAT_V4", "11"); +define("HOTPOT_OUTPUTFORMAT_V5", "12"); +define("HOTPOT_OUTPUTFORMAT_V5_PLUS", "13"); define("HOTPOT_OUTPUTFORMAT_V6", "14"); define("HOTPOT_OUTPUTFORMAT_V6_PLUS", "15"); define("HOTPOT_OUTPUTFORMAT_FLASH", "20"); @@ -30,9 +50,10 @@ $HOTPOT_OUTPUTFORMAT = array ( HOTPOT_OUTPUTFORMAT_BEST => get_string("outputformat_best", "hotpot"), HOTPOT_OUTPUTFORMAT_V6_PLUS => get_string("outputformat_v6_plus", "hotpot"), HOTPOT_OUTPUTFORMAT_V6 => get_string("outputformat_v6", "hotpot"), - // HOTPOT_OUTPUTFORMAT_V5 => get_string("outputformat_v5", "hotpot"), - // HOTPOT_OUTPUTFORMAT_V4 => get_string("outputformat_v4", "hotpot"), - // HOTPOT_OUTPUTFORMAT_V3 => get_string("outputformat_v3", "hotpot"), + HOTPOT_OUTPUTFORMAT_V5_PLUS => get_string("outputformat_v5_plus", "hotpot"), + HOTPOT_OUTPUTFORMAT_V5 => get_string("outputformat_v5", "hotpot"), + HOTPOT_OUTPUTFORMAT_V4 => get_string("outputformat_v4", "hotpot"), + HOTPOT_OUTPUTFORMAT_V3 => get_string("outputformat_v3", "hotpot"), // HOTPOT_OUTPUTFORMAT_FLASH => get_string("outputformat_flash", "hotpot"), // HOTPOT_OUTPUTFORMAT_MOBILE => get_string("outputformat_mobile", "hotpot"), ); @@ -59,7 +80,7 @@ $HOTPOT_NAVIGATION = array ( HOTPOT_NAVIGATION_FRAME => get_string("navigation_frame", "hotpot"), HOTPOT_NAVIGATION_IFRAME => get_string("navigation_iframe", "hotpot"), HOTPOT_NAVIGATION_BUTTONS => get_string("navigation_buttons", "hotpot"), - HOTPOT_NAVIGATION_GIVEUP => get_string("navigation_give_up", "hotpot"), + HOTPOT_NAVIGATION_GIVEUP => get_string("navigation_give_up", "hotpot"), HOTPOT_NAVIGATION_NONE => get_string("navigation_none", "hotpot"), ); @@ -69,8 +90,8 @@ define("HOTPOT_JCROSS", "3"); define("HOTPOT_JMATCH", "4"); define("HOTPOT_JMIX", "5"); define("HOTPOT_JQUIZ", "6"); -define("HOTPOT_TEXTOYS_RHUBARB", "7"); -define("HOTPOT_TEXTOYS_SEQUITUR", "8"); +define("HOTPOT_TEXTOYS_RHUBARB", "7"); +define("HOTPOT_TEXTOYS_SEQUITUR", "8"); define("HOTPOT_JQUIZ_MULTICHOICE", "1"); define("HOTPOT_JQUIZ_SHORTANSWER", "2"); @@ -86,47 +107,630 @@ $HOTPOT_GRADEMETHOD = array ( HOTPOT_GRADEMETHOD_HIGHEST => get_string("gradehighest", "quiz"), HOTPOT_GRADEMETHOD_AVERAGE => get_string("gradeaverage", "quiz"), HOTPOT_GRADEMETHOD_FIRST => get_string("attemptfirst", "quiz"), - HOTPOT_GRADEMETHOD_LAST => get_string("attemptlast", "quiz"), + HOTPOT_GRADEMETHOD_LAST => get_string("attemptlast", "quiz"), ); -function hotpot_add_instance($hp) { +define("HOTPOT_STATUS_INPROGRESS", "1"); +define("HOTPOT_STATUS_TIMEDOUT", "2"); +define("HOTPOT_STATUS_ABANDONED", "3"); +define("HOTPOT_STATUS_COMPLETED", "4"); + +$HOTPOT_STATUS = array ( + HOTPOT_STATUS_INPROGRESS => get_string("inprogress", "hotpot"), + HOTPOT_STATUS_TIMEDOUT => get_string("timedout", "hotpot"), + HOTPOT_STATUS_ABANDONED => get_string("abandoned", "hotpot"), + HOTPOT_STATUS_COMPLETED => get_string("completed", "hotpot"), +); + +define("HOTPOT_FEEDBACK_NONE", "0"); +define("HOTPOT_FEEDBACK_WEBPAGE", "1"); +define("HOTPOT_FEEDBACK_FORMMAIL", "2"); +define("HOTPOT_FEEDBACK_MOODLEFORUM", "3"); +define("HOTPOT_FEEDBACK_MOODLEMESSAGING", "4"); + +$HOTPOT_FEEDBACK = array ( + HOTPOT_FEEDBACK_NONE => get_string("feedbacknone", "hotpot"), + HOTPOT_FEEDBACK_WEBPAGE => get_string("feedbackwebpage", "hotpot"), + HOTPOT_FEEDBACK_FORMMAIL => get_string("feedbackformmail", "hotpot"), + HOTPOT_FEEDBACK_MOODLEFORUM => get_string("feedbackmoodleforum", "hotpot"), +); +if (!empty($CFG->messaging)) { // Moodle 1.5+ + $HOTPOT_FEEDBACK[HOTPOT_FEEDBACK_MOODLEMESSAGING] = get_string("feedbackmoodlemessaging", "hotpot"); +} + +define("HOTPOT_DISPLAYNEXT_QUIZ", "0"); +define("HOTPOT_DISPLAYNEXT_COURSE", "1"); +define("HOTPOT_DISPLAYNEXT_INDEX", "2"); + +////////////////////////////////// +/// CORE FUNCTIONS + + +// possible return values: +// false: +// display moderr.html (if exists) OR "Could not update" and return to couse view +// string: +// display as error message and return to course view +// true (or non-zero number): +// continue to $hp->redirect (if set) OR hotpot/view.php (to displsay quiz) + +function hotpot_add_instance(&$hp) { /// Given an object containing all the necessary data, /// (defined by the form in mod.html) this function /// will create a new instance and return the id number /// of the new instance. - hotpot_set_times($hp); - return insert_record("hotpot", $hp); + if (hotpot_set_form_values($hp)) { + $result = insert_record("hotpot", $hp); + } else { + $result= false; + } + return $result; } -function hotpot_update_instance($hp) { +function hotpot_update_instance(&$hp) { /// Given an object containing all the necessary data, /// (defined by the form in mod.html) this function /// will update an existing instance with new data. - hotpot_set_times($hp); - $hp->id = $hp->instance; - - return update_record("hotpot", $hp); + if (hotpot_set_form_values($hp)) { + $hp->id = $hp->instance; + $result = update_record("hotpot", $hp); + } else { + $result= false; + } + return $result; } -function hotpot_set_times(&$hp) { - $time = time(); +function hotpot_set_form_values(&$hp) { + $ok = true; + $hp->errors = array(); // these will be reported by moderr.html + if (empty($hp->reference)) { + $ok = false; + $hp->errors['reference']= get_string('error_nofilename', 'hotpot'); + } + + if ($hp->studentfeedbackurl=='http://') { + $hp->studentfeedbackurl = ''; + } + + if (empty($hp->studentfeedbackurl)) { + switch ($hp->studentfeedback) { + case HOTPOT_FEEDBACK_WEBPAGE: + $ok = false; + $hp->errors['studentfeedbackurl']= get_string('error_nofeedbackurlwebpage', 'hotpot'); + break; + case HOTPOT_FEEDBACK_FORMMAIL: + $ok = false; + $hp->errors['studentfeedbackurl']= get_string('error_nofeedbackurlformmail', 'hotpot'); + break; + } + } + + $time = time(); $hp->timecreated = $time; $hp->timemodified = $time; - $hp->timeopen = make_timestamp( - $hp->openyear, $hp->openmonth, $hp->openday, - $hp->openhour, $hp->openminute, 0 - ); - $hp->timeclose = make_timestamp( - $hp->closeyear, $hp->closemonth, $hp->closeday, - $hp->closehour, $hp->closeminute, 0 - ); + if (empty($hp->enabletimeopen)) { + $hp->timeopen = 0; + } else { + $hp->timeopen = make_timestamp( + $hp->openyear, $hp->openmonth, $hp->openday, + $hp->openhour, $hp->openminute, 0 + ); + } + + if (empty($hp->enabletimeclose)) { + $hp->timeclose = 0; + } else { + $hp->timeclose = make_timestamp( + $hp->closeyear, $hp->closemonth, $hp->closeday, + $hp->closehour, $hp->closeminute, 0 + ); + } + + if ($hp->quizchain==HOTPOT_YES) { + switch ($hp->mode) { + case 'add': + $ok = hotpot_add_chain($hp); + break; + case 'update': + $ok = hotpot_update_chain($hp); + break; + } + } else { + $xml_quiz = NULL; + + $textfields = array('name', 'summary'); + foreach ($textfields as $textfield) { + + $textsource = $textfield.'source'; + if ($hp->$textsource==HOTPOT_TEXTSOURCE_QUIZ) { + if (empty($xml_quiz)) { + $xml_quiz = new hotpot_xml_quiz($hp, false, false, false, false, false); + hotpot_get_titles_and_next_ex($hp, $xml_quiz->filepath); + } + if ($textfield=='name') { + $hp->$textfield = $hp->exercisetitle; + } else if ($textfield=='summary') { + $hp->$textfield = $hp->exercisesubtitle; + } + } + switch ($hp->$textsource) { + case HOTPOT_TEXTSOURCE_FILENAME: + $hp->$textfield = basename($hp->reference); + break; + case HOTPOT_TEXTSOURCE_FILEPATH: + $hp->$textfield = ''; + // continue to next lines + default: + if (empty($hp->$textfield)) { + $hp->$textfield = str_replace('/', ' ', $hp->reference); + } + } // end switch + } // end foreach + } + + switch ($hp->displaynext) { + // N.B. redirection only works for Moodle 1.5+ + case HOTPOT_DISPLAYNEXT_COURSE: + $hp->redirect = true; + $hp->redirecturl = "view.php?id=$hp->course"; + break; + case HOTPOT_DISPLAYNEXT_INDEX: + $hp->redirect = true; + $hp->redirecturl = "../mod/hotpot/index.php?id=$hp->course"; + break; + // otherwise go on to display quiz + } + + // if ($ok && $hp->setdefaults) { + if ($ok) { + set_user_preference('hotpot_timeopen', $hp->timeopen); + set_user_preference('hotpot_timeclose', $hp->timeclose); + set_user_preference('hotpot_navigation', $hp->navigation); + set_user_preference('hotpot_outputformat', $hp->outputformat); + set_user_preference('hotpot_studentfeedback', $hp->studentfeedback); + set_user_preference('hotpot_studentfeedbackurl', $hp->studentfeedbackurl); + set_user_preference('hotpot_forceplugins', $hp->forceplugins); + set_user_preference('hotpot_shownextquiz', $hp->shownextquiz); + set_user_preference('hotpot_review', $hp->review); + set_user_preference('hotpot_grade', $hp->grade); + set_user_preference('hotpot_grademethod', $hp->grademethod); + set_user_preference('hotpot_attempts', $hp->attempts); + get_user_preference('hotpot_subnet', $hp->subnet); + set_user_preference('hotpot_displaynext', $hp->displaynext); + if ($hp->mode=='add') { + set_user_preference('hotpot_quizchain', $hp->quizchain); + set_user_preference('hotpot_namesource', $hp->namesource); + set_user_preference('hotpot_summarysource', $hp->summarysource); + } + } + + return $ok; +} +function hotpot_get_chain(&$cm) { + + // get details of course_modules in this section + $course_module_ids = get_field('course_sections', 'sequence', 'id', $cm->section); + if (empty($course_module_ids)) { + $hotpot_modules = array(); + } else { + $hotpot_modules = get_records_select('course_modules', "id IN ($course_module_ids) AND module=$cm->module"); + } + + // get ids of hotpot modules in this section + $ids = array(); + foreach ($hotpot_modules as $hotpot_module) { + $ids[] = $hotpot_module->instance; + } + + // get details of hotpots in this section + if (empty($ids)) { + $hotpots = array(); + } else { + $hotpots = get_records_list('hotpot', 'id', implode(',', $ids)); + } + + $found = false; + $chain = array(); + + // loop through course_modules in this section + $ids = explode(',', $course_module_ids); + foreach ($ids as $id) { + + // check this course_module is a hotpot activity + if (isset($hotpot_modules[$id])) { + + // store details of this course module and hotpot activity + $hotpot_id = $hotpot_modules[$id]->instance; + $chain[$id] = &$hotpot_modules[$id]; + $chain[$id]->hotpot = &$hotpots[$hotpot_id]; + + // set $found, if this is the course module we're looking for + if ($id==$cm->coursemodule) { + $found = true; + } + + // is this the end of a chain + if (empty($hotpots[$hotpot_id]->shownextquiz)) { + if ($found) { + break; // out of loop + } else { + // restart chain (target cm has not been found yet) + $chain = array(); + } + } + } + } // end foreach $ids + + return $found ? $chain : false; +} +function hotpot_is_visible(&$cm) { + $visible = HOTPOT_YES; + if (empty($cm->visible)) { + if ($chain = hotpot_get_chain($cm)) { + $visible = $chain[0]->visible; + } + } + return $visible; +} +function hotpot_add_chain(&$hp) { +/// add a chain of hotpot actiivities + + global $CFG, $course; + + $ok = true; + $hp->files = array(); + $hp->titles = array(); + + $xml_quiz = new hotpot_xml_quiz($hp, false, false, false, false, false); + + if (isset($xml_quiz->error)) { + $hp->errors['reference'] = $xml_quiz->error; + $ok = false; + + } else if (is_dir($xml_quiz->filepath)) { + + // get list of hotpot files in this folder + if ($dh = @opendir($xml_quiz->filepath)) { + while ($file = @readdir($dh)) { + if (preg_match('/\.(jbc|jcl|jcw|jmt|jmx|jqz|htm|html)$/', $file)) { + $hp->files[] = "$xml_quiz->reference/$file"; + } + } + closedir($dh); + + // get titles + foreach ($hp->files as $i=>$file) { + $filepath = $xml_quiz->fileroot.DIRECTORY_SEPARATOR.$xml_quiz->filesubdir.$file; + hotpot_get_titles_and_next_ex($hp, $filepath); + $hp->titles[$i] = $hp->exercisetitle; + } + + } else { + $ok = false; + $hp->errors['reference'] = get_string('error_couldnotopenfolder', 'hotpot', $hp->reference); + } + + } else if (is_file($xml_quiz->filepath)) { + + $filerootlength = strlen($xml_quiz->fileroot) + 1; + + while ($xml_quiz->filepath) { + hotpot_get_titles_and_next_ex($hp, $xml_quiz->filepath, true); + + $hp->files[] = substr($xml_quiz->filepath, $filerootlength); + $hp->titles[] = $hp->exercisetitle; + + if ($hp->nextexercise) { + $filepath = $xml_quiz->fileroot.DIRECTORY_SEPARATOR.$xml_quiz->filesubdir.$hp->nextexercise; + } else { + $filepath = ''; + } + if ($filepath && file_exists($filepath) && is_file($filepath) && is_readable($filepath)) { + $xml_quiz->filepath = $filepath; + } else { + $xml_quiz->filepath = false; // finish while loop + } + } // end while + + } else { + $ok = false; + $hp->errors['reference'] = get_string('error_notfileorfolder', 'hotpot', $hp->reference); + } + + if (empty($hp->files) && empty($hp->errors['reference'])) { + $ok = false; + $hp->errors['reference'] = get_string('error_noquizzesfound', 'hotpot', $hp->reference); + } + + if ($ok) { + $hp->visible = HOTPOT_YES; + $hp->shownextquiz = HOTPOT_YES; + + if (trim($hp->name)=='') { + $hp->name = get_string("modulename", $hp->modulename); + } + $hp->basename = $hp->name; + + // add all except last activity in chain + + $i_max = count($hp->files)-1; + for ($i=0; $i<$i_max; $i++) { + + $hp->name = addslashes($hp->titles[$i]); + $hp->reference = addslashes($hp->files[$i]); + + if (!$hp->instance = insert_record("hotpot", $hp)) { + error("Could not add a new instance of $hp->modulename", "view.php?id=$hp->course"); + } + + // store (hotpot table) id of start of chain + if ($i==0) { + $hp->startofchain = $hp->instance; + } + + if (isset($course->groupmode)) { + $hp->groupmode = $course->groupmode; + } + + if (! $hp->coursemodule = add_course_module($hp)) { + error("Could not add a new course module"); + } + if (! $sectionid = add_mod_to_section($hp) ) { + error("Could not add the new course module to that section"); + } + + if (! set_field("course_modules", "section", $sectionid, "id", $hp->coursemodule)) { + error("Could not update the course module with the correct section"); + } + + add_to_log($hp->course, "course", "add mod", + "../mod/$hp->modulename/view.php?id=$hp->coursemodule", + "$hp->modulename $hp->instance" + ); + add_to_log($hp->course, $hp->modulename, "add", + "view.php?id=$hp->coursemodule", + "$hp->instance", $hp->coursemodule + ); + + // hide tail of chain + $hp->visible = HOTPOT_NO; + + } // end for ($hp->files) + + // settings for final activity in chain + $hp->name = addslashes($hp->titles[$i]); + $hp->reference = addslashes($hp->files[$i]); + $hp->shownextquiz = HOTPOT_NO; + + if (isset($hp->startofchain)) { + // redirection only works for Moodle 1.5+ + $hp->redirect = true; + $hp->redirecturl = "$CFG->wwwroot/mod/hotpot/view.php?hp=$hp->startofchain"; + } + } // end if $ok + + return $ok; +} +function hotpot_get_titles_and_next_ex(&$hp, $filepath, $get_next=false) { + + $hp->exercisetitle = ''; + $hp->exercisesubtitle = ''; + $hp->nextexercise = ''; + + // open the quiz file + if ($fp = @fopen($filepath, 'r')) { + + $source = fread($fp, filesize($filepath)); + fclose($fp); + + $xml_tree = new hotpot_xml_tree($source); + $xml_tree->filetype = ''; + + $keys = array_keys($xml_tree->xml); + foreach ($keys as $key) { + + if ($key=='html' || $key=='HTML') { + $xml_tree->filetype = 'html'; + $xml_tree->xml_root = "['$key']['#']"; + break; + } else if (preg_match('/^(hotpot|textoys)-(\w+)-file$/i', $key, $matches)) { + $xml_tree->filetype = 'xml'; + $xml_tree->xml_root = "['$key']['#']"; + $xml_tree->quiztype = strtolower($matches[2]); + break; + } + } + + $title = ''; + if ($xml_tree->filetype=='html') { + $title = strip_tags($xml_tree->xml_value('head,title')); + } else if ($xml_tree->filetype=='xml') { + $title = strip_tags($xml_tree->xml_value('data,title')); + } + $hp->exercisetitle = (empty($title) || is_array($title)) ? basename($filepath) : $title; + + $subtitle = ''; + if ($xml_tree->filetype=='html') { + $tags = 'body,div'; + + $i = 0; + while (empty($subtitle) && ($div="[$i]") && $xml_tree->xml_value($tags, $div)) { + + $class = $xml_tree->xml_value($tags, $div."['@']['class']"); + if (isset($class) && $class=='Titles') { + + $ii = 0; + while (empty($subtitle) && ($h3=$div."['#']['h3'][$ii]") && $xml_tree->xml_value($tags, $h3)) { + + $class = $xml_tree->xml_value($tags, $h3."['@']['class']"); + if (isset($class) && $class=='ExerciseSubtitle') { + $subtitle = $xml_tree->xml_value($tags, $h3."['#']"); + } + + $ii++; // increment H3 index + } + } + $i++; // increment DIV index + } + } else if ($xml_tree->filetype=='xml') { + $subtitle = $xml_tree->xml_value('hotpot-config-file,'.$xml_tree->quiztype.',exercise-subtitle'); + } + $hp->exercisesubtitle = (empty($subtitle) || is_array($subtitle)) ? $hp->exercisetitle : $subtitle; + + $next = ''; + if ($get_next) { + + if ($xml_tree->filetype=='html') { + $tags = 'body,div'; + + $i = 0; + while (($div="[$i]") && $xml_tree->xml_value($tags, $div)) { + + $id = $xml_tree->xml_value($tags, $div."['@']['id']"); + if (isset($id) && $id=='TopNavBar') { + + $ii = 0; + while (($button=$div."['#']['button'][$ii]") && $xml_tree->xml_value($tags, $button)) { + + $onclick = $xml_tree->xml_value($tags, $button."['@']['onclick']"); + if (isset($onclick) && preg_match("|location='(.*)'|", $onclick, $matches)) { + $next = $matches[1]; + } + + $ii++; // increment BUTTON index + } + } + $i++; // increment DIV index + } + + } else if ($xml_tree->filetype=='xml') { + + $include = $xml_tree->xml_value('hotpot-config-file,global,include-next-ex'); + if (!empty($include)) { + + $next = $xml_tree->xml_value("hotpot-config-file,$xml_tree->quiztype,next-ex-url"); + if (is_array($next)) { + // workaround for the 'next-ex-url' tag being repeated (as it sometimes seems to be) + $next = $next[0]; + } + } + } + } + $hp->nextexercise = $next; + } +} +function hotpot_get_all_instances_in_course($modulename, $course) { + + global $CFG; + $instances = array(); + + if ($modinfo = unserialize($course->modinfo)) { + + if (isset($CFG->release) && substr($CFG->release, 0, 3)>=1.2) { + $groupmode = 'cm.groupmode,'; + } else { + $groupmode = ''; + } + $query = " + SELECT + cm.id AS coursemodule, + cm.visible AS visible, + $groupmode + cs.section, + m.* + FROM + {$CFG->prefix}course_modules AS cm, + {$CFG->prefix}course_sections AS cs, + {$CFG->prefix}modules AS md, + {$CFG->prefix}$modulename AS m + WHERE + cm.course = '$course->id' AND + cm.instance = m.id AND + cm.section = cs.id AND + md.name = '$modulename' AND + md.id = cm.module + "; + if ($rawmods = get_records_sql($query)) { + + // cache $isteacher setting + $isteacher = isteacher($course->id); + + foreach ($modinfo as $mod) { + + $visible = false; + if ($mod->mod == $modulename) { + if ($isteacher) { + $visible = true; + } else if ($mod->mod=='hotpot') { + $visible = hotpot_is_visible($mod); + } else { + $visible = $mod->visible; + } + } + if ($visible) { + $instance = $rawmods[$mod->cm]; + if (!empty($mod->extra)) { + $instance->extra = $mod->extra; + } + $instances[] = $instance; + } + } // end foreach $modinfo + } + } + return $instances; } +function hotpot_update_chain(&$hp) { +/// update a chain of hotpot actiivities + + $ok = true; + if ($hotpot_modules = hotpot_get_chain($hp)) { + + // skip updating of these fields + $skipfields = array('id', 'course', 'name', 'reference', 'summary', 'shownextquiz'); + $fields = array(); + + foreach ($hotpot_modules as $hotpot_module) { + + if ($hp->coursemodule==$hotpot_module->id) { + // don't need to update this hotpot + + } else { + // shortcut to hotpot record + $hotpot = &$hotpot_module->hotpot; + + // get a list of fields to update + if (empty($fields)) { + $fields = array_keys(get_object_vars($hotpot)); + } + + // assume update is NOT required + $require_update = false; + + // update field values (except $skipfields) + foreach($fields as $field) { + if (in_array($field, $skipfields) || $hotpot->$field==$hp->$field) { + // update not required for this field + } else { + $require_update = true; + $hotpot->$field = $hp->$field; + } + } + + // update this $hotpot (if required) + if ($require_update && !update_record("hotpot", $hotpot)) { + error("Could not update the $hp->modulename", "view.php?id=$hp->course"); + } + } + } // end foreach $ids + } + return $ok; +} function hotpot_delete_instance($id) { /// Given an ID of an instance of this module, /// this function will permanently delete the instance @@ -135,10 +739,11 @@ function hotpot_delete_instance($id) { $result = false; if (delete_records("hotpot", "id", "$id")) { $result = true; + delete_records("hotpot_questions", "hotpot", "$id"); if ($attempts = get_records_select("hotpot_attempts", "hotpot='$id'")) { $ids = implode(',', array_keys($attempts)); - delete_records_select("hotpot_attempts", "id IN ($ids)"); - delete_records_select("hotpot_questions", "attempt IN ($ids)"); + delete_records_select("hotpot_attempts", "id IN ($ids)"); + delete_records_select("hotpot_details", "attempt IN ($ids)"); delete_records_select("hotpot_responses", "attempt IN ($ids)"); } } @@ -150,7 +755,7 @@ function hotpot_delete_and_notify($table, $select, $strtable) { delete_records_select($table, $select); $count -= max(0, count_records_select($table, $select)); if ($count) { - notify(get_string("deleted")." $count x $strtable"); + notify(get_string('deleted')." $count x $strtable"); } } } @@ -195,16 +800,26 @@ function hotpot_user_outline($course, $user, $mod, $hp) { return $report; } -function hotpot_format_score($record) { +function hotpot_format_score($record, $undefined=' ') { if (isset($record->score)) { $score = $record->score; } else { - $str = empty($record->timefinish) ? 'inprogress' : 'abandoned'; - $score = ''.get_string($str, 'hotpot').''; + $score = $undefined; } return $score; } +function hotpot_format_status($record, $undefined=' ') { + global $HOTPOT_STATUS; + + if (isset($record->status) || isset($HOTPOT_STATUS[$record->status])) { + $status = $HOTPOT_STATUS[$record->status]; + } else { + $status = $undefined; + } + return $status; +} + function hotpot_print_recent_activity($course, $isteacher, $timestart) { /// Given a course and a time, this module should find recent activity /// that has occurred in hotpot activities and print it out. @@ -226,6 +841,7 @@ function hotpot_print_recent_activity($course, $isteacher, $timestart) { WHERE h.course = $course->id AND h.id = a.hotpot + AND a.id = a.clickreportid AND a.starttime > $timestart GROUP BY h.id, h.name @@ -236,16 +852,21 @@ function hotpot_print_recent_activity($course, $isteacher, $timestart) { $names = array(); foreach ($records as $id => $record){ - $href = $CFG->wwwroot.'/mod/hotpot/view.php?id='.$id; - $name = " $record->name"; + $href = "$CFG->wwwroot/mod/hotpot/view.php?hp=$id"; + $name = ' '.$record->name.''; if ($record->count_attempts > 1) { $name .= " ($record->count_attempts)"; } $names[] = $name; } - print_headline(get_string('modulenameplural', 'hotpot').':'); - echo '
'.implode('
', $names).'
'; + print_headline(get_string('modulenameplural', 'hotpot').':'); + + if ($CFG->version >= 2005050500) { // Moodle 1.5+ + echo '
'.implode('
', $names).'
'; + } else { // Moodle 1.4.x (or less) + echo ''.implode('
', $names).'
'; + } $result = true; } @@ -275,6 +896,7 @@ function hotpot_get_recent_mod_activity(&$activities, &$index, $sincetime, $cour {$CFG->prefix}user AS u WHERE a.timefinish > '$sincetime' + AND a.id = a.clickreportid AND a.userid = u.id $user_select AND a.hotpot = h.id $cm_select AND cm.instance = h.id @@ -300,7 +922,6 @@ function hotpot_get_recent_mod_activity(&$activities, &$index, $sincetime, $cour $activity->content->attemptid = $record->id; $activity->content->attempt = $record->attempt; $activity->content->score = $record->score; - $activity->content->details = $record->details; $activity->content->timestart = $record->timestart; $activity->content->timefinish = $record->timefinish; @@ -335,7 +956,7 @@ function hotpot_print_recent_mod_activity($activity, $course, $detail=false) { print ''.$activity->type.' '; // link to activity - $href = "$CFG->wwwroot/mod/hotpot/view.php?id=$activity->instance"; + $href = "$CFG->wwwroot/mod/hotpot/view.php?hp=$activity->instance"; print ''.$activity->name.' - '; } if (isteacher($course)) { @@ -388,7 +1009,7 @@ function hotpot_get_grades($hotpot, $user_ids='') { $grades = array(); $weighting = $hotpot->grade / 100; - $precision = ($hotpot->grademethod==HOTPOT_GRADEMETHOD_AVERAGE || $hotpot->grade<100) ? 1 : 0; + $precision = hotpot_get_precision($hotpot); // set the SQL string to determine the $grade $grade = ""; @@ -418,24 +1039,28 @@ function hotpot_get_grades($hotpot, $user_ids='') { if ($grade) { $userid_condition = empty($user_ids) ? '' : "AND userid IN ($user_ids) "; - $grades = get_records_sql_menu(" SELECT userid, $grade FROM {$CFG->prefix}hotpot_attempts WHERE timefinish>0 AND hotpot='$hotpot->id' $userid_condition GROUP BY userid "); - - if ($hotpot->grademethod==HOTPOT_GRADEMETHOD_FIRST || $hotpot->grademethod==HOTPOT_GRADEMETHOD_LAST) { - // remove left hand characters in $grade (up to and including the underscore) - foreach ($grades as $userid=>$grade) { - $grades[$userid] = substr($grades[$userid], strpos($grades[$userid], '_')+1); + if ($grades) { + if ($hotpot->grademethod==HOTPOT_GRADEMETHOD_FIRST || $hotpot->grademethod==HOTPOT_GRADEMETHOD_LAST) { + // remove left hand characters in $grade (up to and including the underscore) + foreach ($grades as $userid=>$grade) { + $grades[$userid] = substr($grades[$userid], strpos($grades[$userid], '_')+1); + } } } } return $grades; } +function hotpot_get_precision(&$hotpot) { + return ($hotpot->grademethod==HOTPOT_GRADEMETHOD_AVERAGE || $hotpot->grade<100) ? 1 : 0; +} + function hotpot_get_participants($hotpotid) { //Must return an array of user records (all data) who are participants //for a given instance of hotpot. Must include every user involved @@ -472,48 +1097,58 @@ function hotpot_scale_used ($hotpotid, $scaleid) { return $report; } -////////////////////////////////////////////////////////////////////////////////////// -/// Any other hotpot functions go here. Each of them must have a name that -/// starts with hotpot +////////////////////////////////////////////////////////// +/// Any other hotpot functions go here. +/// Each of them must have a name that starts with hotpot -function hotpot_add_attempt($hotpotid, $userid=NULL) { - $userid = hotpot_get_userid($userid); +function hotpot_add_attempt($hotpotid) { + global $db, $CFG, $USER; + $time = time(); + switch (strtolower($CFG->dbtype)) { + case 'mysql': + $timefinish = "IF(a.timefinish IS NULL, '$time', a.timefinish)"; + $clickreportid = "IF(a.clickreportid IS NULL, a.id, a.clickreportid)"; + break; + case 'postgres7': + $timefinish = "WHEN(a.timefinish IS NULL) THEN '$time' ELSE a.timefinish"; + $clickreportid = "WHEN(a.clickreportid IS NULL) THEN a.id ELSE a.clickreportid"; + break; + } - hotpot_close_previous_attempts($hotpotid, $userid); + $db->Execute(" + UPDATE + {$CFG->prefix}hotpot_attempts as a + SET + a.timefinish = $timefinish, + a.status = '".HOTPOT_STATUS_ABANDONED."', + a.clickreportid = $clickreportid + WHERE + a.hotpot='$hotpotid' + AND a.userid='$USER->id' + AND a.status='".HOTPOT_STATUS_INPROGRESS."' + "); + // create and add new attempt record $attempt->hotpot = $hotpotid; - $attempt->userid = $userid; - $attempt->attempt = hotpot_get_next_attempt($hotpotid, $userid); + $attempt->userid = $USER->id; + $attempt->attempt = hotpot_get_next_attempt($hotpotid); $attempt->timestart = time(); return insert_record("hotpot_attempts", $attempt); } -function hotpot_close_previous_attempts($hotpotid, $userid=NULL, $time=NULL) { -/// set previously unfinished attempts of this quiz by this user to "finished" - if (empty($time)) { - $time = time(); - } - $userid = hotpot_get_userid($userid); - set_field("hotpot_attempts", "timefinish", $time, "hotpot", $hotpotid, "userid", $userid, "timefinish", 0); -} -function hotpot_get_next_attempt($hotpotid, $userid=NULL) { - // get max attempt so far - $i = count_records_select( - 'hotpot_attempts', - "hotpot='$hotpotid' AND userid='".hotpot_get_userid($userid)."'", - 'MAX(attempt)' - ); - return empty($i) ? 1 : ($i+1); -} -function hotpot_get_userid($userid=NULL) { +function hotpot_get_next_attempt($hotpotid) { global $USER; - return isset($userid) ? $userid : $USER->id; + + // get max attempt so far + $i = count_records_select('hotpot_attempts', "hotpot='$hotpotid' AND userid='$USER->id'", 'MAX(attempt)'); + + return empty($i) ? 1 : ($i+1); } function hotpot_get_question_name($question) { $name = ''; if (isset($question->text)) { - $name =hotpot_strings($question->text); + $name = hotpot_strings($question->text); } if (empty($name)) { $name = $question->name; @@ -526,8 +1161,9 @@ function hotpot_strings($ids) { static $HOTPOT_EMPTYSTRINGS; if (!isset($HOTPOT_EMPTYSTRINGS)) { // first time only - $emptystringids = get_records_select('hotpot_strings', 'LENGTH(TRIM(string))=0'); - $HOTPOT_EMPTYSTRINGS = empty($emptystringids) ? array() : array_keys($emptystringids); + // get ids of empty strings + $emptystrings = get_records_select('hotpot_strings', 'LENGTH(TRIM(string))=0'); + $HOTPOT_EMPTYSTRINGS = empty($emptystrings) ? array() : array_keys($emptystrings); } $strings = array(); @@ -551,6 +1187,9 @@ function hotpot_string($id) { // get the standard XML parser supplied with Moodle require_once($CFG->libdir.DIRECTORY_SEPARATOR.'xmlize.php'); +// get the default class for hotpot quiz templates +require_once($CFG->hotpottemplate.DIRECTORY_SEPARATOR.'default.php'); + class hotpot_xml_tree { function hotpot_xml_tree($str, $xml_root='') { if (empty($str)) { @@ -562,22 +1201,61 @@ class hotpot_xml_tree { $this->xml_root = $xml_root; } function xml_value($tags, $more_tags="[0]['#']") { + $tags = empty($tags) ? '' : "['".str_replace(",", "'][0]['#']['", $tags)."']"; eval('$value = &$this->xml'.$this->xml_root.$tags.$more_tags.';'); + if (is_string($value)) { $value = utf8_decode($value); - // decode angle brackets and replace newlines - $value = strtr($value, array('<'=>'<', '>'=>'>', "\n"=>'
')); + // decode angle brackets + $value = strtr($value, array('<'=>'<', '>'=>'>')); + + // remove white space between , and parts + // (so it doesn't get converted to
) + $htmltags = '(' + . 'TABLE|/?CAPTION|/?COL|/?COLGROUP|/?TBODY|/?TFOOT|/?THEAD|/?TD|/?TH|/?TR' + . '|OL|UL|/?LI' + . '|DL|/?DT|/?DD' + . '|EMBED|OBJECT|APPLET|/?PARAM' + //. '|SELECT|/?OPTION' + //. '|FIELDSET|/?LEGEND' + //. '|FRAMESET|/?FRAME' + . ')' + ; + $search = '#(<'.$htmltags.'[^>]*'.'>)\s+'.'(?='.'<'.')#is'; + $value = preg_replace($search, '\\1', $value); + + // replace remaining newlines with
+ $value = str_replace("\n", '
', $value); + + // encode unicode characters as HTML entities + // (in particular, accented charaters that have not been encoded by HP) + + // unicode characetsr can be detected by checking the hex value of a character + // 00 - 7F : ascii char (roman alphabet + punctuation) + // 80 - BF : byte 2, 3 or 4 of a unicode char + // C0 - DF : 1st byte of 2-byte char + // E0 - EF : 1st byte of 3-byte char + // F0 - FF : 1st byte of 4-byte char + // if the string doesn't match the above, it might be + // 80 - FF : single-byte, non-ascii char + $search = '#('.'[\xc0-\xdf][\x80-\xbf]'.'|'.'[\xe0-\xef][\x80-\xbf]{2}'.'|'.'[\xf0-\xff][\x80-\xbf]{3}'.'|'.'[\x80-\xff]'.')#se'; + $value = preg_replace($search, "hotpot_utf8_to_html_entity('\\1')", $value); + + // NOTICE + // ====== + // the following lines have been removed because + // the final "preg_replace" takes several SECONDS to run // encode any orphaned angle brackets back to html entities - if (empty($this->tag_pattern)) { - $q = "'"; // single quote - $qq = '"'; // double quote - $this->tag_pattern = '<(([^>'.$q.$qq.']*)|('."{$q}[^$q]*$q".')|('."{$qq}[^$qq]*$qq".'))*>'; - } - $value = preg_replace('/<([^>]*'.$this->tag_pattern.')/', '<$1', $value); - $value = preg_replace('/('.$this->tag_pattern.'[^<]*)>/', '$1>', $value); + //if (empty($this->tag_pattern)) { + // $q = "'"; // single quote + // $qq = '"'; // double quote + // $this->tag_pattern = '<(([^>'.$q.$qq.']*)|('."{$q}[^$q]*$q".')|('."{$qq}[^$qq]*$qq".'))*>'; + //} + //$value = preg_replace('/<([^>]*'.$this->tag_pattern.')/', '<$1', $value); + //$value = preg_replace('/('.$this->tag_pattern.'[^<]*)>/', '$1>', $value); } return $value; } @@ -595,14 +1273,14 @@ class hotpot_xml_tree { function encode_cdata(&$str, $tag) { // conversion tables - $HTML_ENTITIES = array( + static $HTML_ENTITIES = array( ''' => "'", '"' => '"', '<' => '<', '>' => '>', '&' => '&', ); - $ILLEGAL_STRINGS = array( + static $ILLEGAL_STRINGS = array( "\r" => '', "\n" => '<br />', ']]>' => ']]>', @@ -630,7 +1308,7 @@ class hotpot_xml_tree { class hotpot_xml_quiz extends hotpot_xml_tree { // constructor function - function hotpot_xml_quiz(&$obj) { + function hotpot_xml_quiz(&$obj, $read_file=true, $parse_xml=true, $convert_urls=true, $report_errors=true, $create_html=true) { // obj can be the $_GET array or a form object/array global $CFG, $HOTPOT_OUTPUTFORMAT, $HOTPOT_OUTPUTFORMAT_DIR; @@ -638,11 +1316,18 @@ class hotpot_xml_quiz extends hotpot_xml_tree { // check xmlize functions are available if (! function_exists("xmlize")) { error('xmlize functions are not available'); - } + } + + $this->read_file = $read_file; + $this->parse_xml = $parse_xml; + $this->convert_urls = $convert_urls; + $this->report_errors = $report_errors; + $this->create_html = $create_html; // extract fields from $obj // course : the course id - // reference : the filename within the files folder for this course + // reference : the filename within the files folder + // location : "site" files folder or "course" files folder // navigation : type of navigation required in quiz // forceplugins : force Moodle compatible media players $this->course = $this->obj_value($obj, 'course'); @@ -653,7 +1338,11 @@ class hotpot_xml_quiz extends hotpot_xml_tree { // can't continue if there is no course or reference if (empty($this->course) || empty($this->reference)) { - error('Could not create XML tree: missing course or reference'); + $this->error = get_string('error_nocourseorfilename', 'hotpot'); + if ($this->report_errors) { + error($this->error); + } + return; } $this->course_homeurl = "$CFG->wwwroot/course/view.php?id=$this->course"; @@ -670,1548 +1359,239 @@ class hotpot_xml_quiz extends hotpot_xml_tree { $this->filedir = $this->course; break; } + $this->filesubdir = dirname($this->reference); + if ($this->filesubdir=='.') { + $this->filesubdir = ''; + } + if ($this->filesubdir) { + $this->filesubdir .= DIRECTORY_SEPARATOR; + } $this->filename = basename($this->reference); - $this->filepath = $CFG->dataroot.DIRECTORY_SEPARATOR.$this->filedir.DIRECTORY_SEPARATOR.$this->reference; + $this->fileroot = $CFG->dataroot.DIRECTORY_SEPARATOR.$this->filedir; + $this->filepath = $this->fileroot.DIRECTORY_SEPARATOR.$this->reference; - // try and open the file - if (!file_exists($this->filepath) || !$fp = fopen($this->filepath, 'r')) { - error('Could not open the XML source file "'.$this->filename.'"', $this->course_homeurl); - } - - // read in the XML source and close the file - $this->source = fread($fp, filesize($this->filepath)); - fclose($fp); - - // convert relative URLs to absolute URLs - $this->hotpot_convert_relative_urls($this->source); - - // encode "gap fill" text in JCloze exercise - $this->encode_cdata($this->source, 'gap-fill'); - - // convert source to xml tree - $this->hotpot_xml_tree($this->source); - - // initialize file type, quiz type and output format - $this->html = ''; - $this->filetype = ''; - $this->quiztype = ''; - $this->outputformat = 0; - - // link tag to , if necessary - if (isset($this->xml['HTML'])) { - $this->xml['html'] = &$this->xml['HTML']; - } - - if (isset($this->xml['html'])) { - - $this->filetype = 'html'; - - // shortcut to source - $s = &$this->source; - - // try to set the quiz type from phrases in the source - - if (strpos($s, 'QuizForm') && strpos($s, 'CheckForm') && strpos($s, 'CorrectAnswers')) { - $this->outputformat = HOTPOT_OUTPUTFORMAT_V3; - $this->quiztype = 'jmatch'; - - } else if (strpos($s, 'name="FeedbackFrame"') && strpos($s, 'name="CodeFrame"')) { - $this->outputformat = HOTPOT_OUTPUTFORMAT_V3; - $this->quiztype = strpos($s, 'QuizForm') ? 'jcb' : strpos($s, 'Cloze') ? 'jcloze' : strpos($s, 'Crossword') ? 'jcross' : strpos($s, 'QForm1') ? 'jquiz' : ''; - - } else if (strpos($s, 'function DynLayer')) { - $this->outputformat = HOTPOT_OUTPUTFORMAT_V4; - $this->quiztype = (strpos($s, 'QForm') && strpos($s, 'QForm.FB[QNum]')) ? 'jcb' : strpos($s, 'Cloze') ? 'jcloze' : strpos($s, 'Crossword') ? 'jcross' : strpos($s, 'ExCheck') ? 'jmatch' : (strpos($s, 'QForm') && strpos($s, 'QForm.Answer')) ? 'jquiz' : ''; - - } else if (strpos($s, 'name="TopFrame"') && strpos($s, 'name="BottomFrame"')) { - $this->outputformat = HOTPOT_OUTPUTFORMAT_V5; - $this->quiztype = (strpos($s, 'QForm') && strpos($s, 'FB_[QNum]_[ANum]')) ? 'jcb' : strpos($s, 'form name="Cloze"') ? 'jcloze' : strpos($s, 'AnswerForm') ? 'jcross' : (strpos($s, 'QForm') && strpos($s, 'sel[INum]')) ? 'jmatch' : strpos($s, 'ButtonForm') ? 'jmix' : (strpos($s, 'QForm[QNum]') && strpos($s, 'Buttons[QNum]')) ? 'jquiz' : ''; - - } else if (strpos($s, '
')) { - $this->outputformat = HOTPOT_OUTPUTFORMAT_V6; - $this->quiztype = strpos($s, 'jcb test') ? 'jcb' : strpos($s, '
') ? 'jcloze' : (strpos($s, 'GridDiv') || strpos($s, 'Clues')) ? 'jcross' : strpos($s, 'MatchDiv') ? 'jmatch' : strpos($s, 'SegmentDiv') ? 'jmix' : ((strpos($s, 'QForm') && strpos($s, 'QForm.Guess')) || strpos($s, 'Questions')) ? 'jquiz' : ''; - - } else if (strpos($s, '
')) { // TexToys - $this->outputformat = HOTPOT_OUTPUTFORMAT_V6; - $this->quiztype = strpos($s, 'var Words = new Array()') ? 'rhubarb' : strpos($s, 'var Segments = new Array()') ? 'sequitur' : ''; - - } else if (strpos($s, 'D = new Array')) { - $this->outputformat = HOTPOT_OUTPUTFORMAT_V6_PLUS; // drag and drop (HP5 and HP6) - $this->quiztype = (strpos($s, 'F = new Array')) ? 'jmatch' : (strpos($s, 'Drop = new Array')) ? 'jmix' : 0; - } - - unset($s); - - } else { - $this->filetype = 'xml'; - - $keys = array_keys($this->xml); - foreach ($keys as $key) { - if (preg_match('/^(hotpot|textoys)-(\w+)-file$/i', $key, $matches)) { - $this->quiztype = strtolower($matches[2]); - $this->xml_root = "['$key']['#']"; - break; + // read the file, if required + if ($this->read_file) { + + if (!file_exists($this->filepath) || !$fp = fopen($this->filepath, 'r')) { + $this->error = get_string('error_couldnotopensourcefile', 'hotpot', $this->filepath); + if ($this->report_errors) { + error($this->error, $this->course_homeurl); } + return; } - } - - - // set the real output format from the requested output format - $this->real_outputformat = $this->obj_value($obj, 'outputformat'); - $this->draganddrop = ''; - if ( - empty($this->real_outputformat) || - $this->real_outputformat==HOTPOT_OUTPUTFORMAT_BEST || - empty($HOTPOT_OUTPUTFORMAT_DIR[$this->real_outputformat]) - ) { - // set the best output format for this browser - // see http://jp2.php.net/function.get-browser - if (function_exists('get_browser') && ini_get('browscap')) { - $b = get_browser(); - // apparently get_browser is a slow - // so we should store the results in $this->browser - } else { - $ua = $_SERVER['HTTP_USER_AGENT']; - $b = NULL; - // store the results in $this->browser - // [parent] => Firefox 0.9 - // [platform] => WinXP - // [browser] => Firefox - // [version] => 0.9 - // [majorver] => 0 - // [minorver] => 9 - } - if ($this->quiztype=='jmatch' || $this->quiztype=='jmix') { - $this->real_outputformat = HOTPOT_OUTPUTFORMAT_V6_PLUS; - } else { - $this->real_outputformat = HOTPOT_OUTPUTFORMAT_V6; - } - } - - if ($this->real_outputformat==HOTPOT_OUTPUTFORMAT_V6_PLUS) { - if ($this->quiztype=='jmatch' || $this->quiztype=='jmix') { - $this->draganddrop = 'd'; // prefix for templates (can also be "f" ?) - } - $this->real_outputformat = HOTPOT_OUTPUTFORMAT_V6; - } - - // set template source directory - $this->template_dir = $HOTPOT_OUTPUTFORMAT_DIR[$this->real_outputformat]; - $this->template_dir_path = $CFG->hotpotroot.DIRECTORY_SEPARATOR.$this->template_dir.DIRECTORY_SEPARATOR.'source'; - - // set the output html - $this->html = ''; - if ($this->filetype=='html') { - $this->html = &$this->source; - - } else { - $method = $this->template_dir.'_create_html'; - if (method_exists($this, $method)) { - eval('$this->'.$method.'();'); - } else { - error( - $method.'Could not create quiz in "'.$this->template_dir.'" format', - $this->course_homeurl - ); - } - } - - } // end constructor function - - function read_template($filename, $tag='temporary') { - // create the file path to the template - $filepath = $this->template_dir_path.DIRECTORY_SEPARATOR.$filename; - - // try and open the template file - if (!file_exists($filepath) || !$fp = fopen($filepath, "r")) { - error( - 'Could not open the '.$this->template_dir.' template file "'.$filename.'"', - $this->course_homeurl - ); - } - - // read in the template and close the file - $this->$tag = fread($fp, filesize($filepath)); - fclose($fp); - - // expand the blocks and strings in the template - $this->expand_blocks($tag); - $this->expand_strings($tag); - - if ($tag=='temporary') { - $template = $this->$tag; - $this->$tag = ''; - return $template; - } - - } - function expand_blocks($tag) { - // get block $names - // [1] the full block name (including optional leading 'str' or 'incl') - // [2] leading 'incl' or 'str' - // [3] the real block name ([1] without [2]) - $search = '/\[\/((incl|str)?(\w+))\]/'; - preg_match_all($search, $this->$tag, $names); - - $i_max = count($names[0]); - for ($i=0; $i<$i_max; $i++) { - - $method = $this->template_dir.'_expand_'.$names[3][$i]; - if (method_exists($this, $method)) { - - eval('$value=$this->'.$method.'();'); - - $search = '/\['.$names[1][$i].'\](.*?)\[\/'.$names[1][$i].'\]/s'; - preg_match_all($search, $this->$tag, $blocks); - - $ii_max = count($blocks[0]); - for ($ii=0; $ii<$ii_max; $ii++) { - - $replace = empty($value) ? '' : $blocks[1][$ii]; - $this->$tag = str_replace($blocks[0][$ii], $replace, $this->$tag); - } - } else { - error('Could not expand template block "'.$matches[4][$i].'"', $this->course_homeurl); - //print 'Could not expand template block "'.$blockname.'" for '.$tag."
\n"; - } - } - } - function expand_strings($tag, $search='') { - if (empty($search)) { - // default $search $pattern - $search = '/\[(?:bool|int|str)(\\w+)\]/'; - } - preg_match_all($search, $this->$tag, $matches); - - $i_max = count($matches[0]); - for ($i=0; $i<$i_max; $i++) { - - $method = $this->template_dir.'_expand_'.$matches[1][$i]; - if (method_exists($this, $method)) { - - eval('$replace=$this->'.$method.'();'); - $this->$tag = str_replace($matches[0][$i], $replace, $this->$tag); - } - } - } - - function bool_value($tags, $more_tags="[0]['#']") { - $value = $this->xml_value($tags, $more_tags); - return empty($value) ? 'false' : 'true'; - } - function int_value($tags, $more_tags="[0]['#']") { - return intval($this->xml_value($tags, $more_tags)); - } - function js_value($tags, $more_tags="[0]['#']", $convert_to_unicode=false) { - return $this->js_safe($this->xml_value($tags, $more_tags), $convert_to_unicode); - } - function js_safe($str, $convert_to_unicode=false) { - // encode a string for javascript - - // decode "<" and ">" - not necesary as it was done by xml_value() - // $str = strtr($str, array('<' => '<', '>' => '>')); - - // escape single quotes and backslashes - $str = strtr($str, array("'"=>"\\'", '\\'=>'\\\\')); - - // convert newlines (win = "\r\n", mac="\r", linix/unix="\n") - $nl = '\\n'; // javascript newline - $str = strtr($str, array("\r\n"=>$nl, "\r"=>$nl, "\n"=>$nl)); - - // convert (hex and decimal) html entities to unicode, if required - if ($convert_to_unicode) { - $str = preg_replace('|&#x([0-9A-F]+);|i', '\\u\\1', $str); - $str = preg_replace('|&#(\d+);|e', "'\\u'.sprintf('%04X', '\\1')", $str); - } - - return $str; - } - - // ================================= - // functions for v6 quizzes - // ================================= - - function v6_create_html() { - - if (isset($_GET['css'])) { - $this->css = ''; - $this->read_template('hp6.cs_', 'css'); - - } else if (isset($_GET['js'])) { - $this->js = ''; - $this->read_template($this->draganddrop.$this->quiztype.'6.js_', 'js'); - - } else { - $this->html = ''; - $this->read_template($this->draganddrop.$this->quiztype.'6.ht_', 'html'); - } - - // expand special strings, if any - $pattern = ''; - switch ($this->quiztype) { - case 'jcloze': - $pattern = '/\[(PreloadImageList)\]/'; - break; - case 'jcross': - $pattern = '/\[(PreloadImageList|ShowHideClueList)\]/'; - break; - case 'jmatch': - $pattern = '/\[(PreloadImageList|QsToShow|FixedArray|DragArray)\]/'; - break; - case 'jmix': - $pattern = '/\[(PreloadImageList|SegmentArray|AnswerArray)\]/'; - break; - case 'jquiz': - $pattern = '/\[(PreloadImageList|QsToShow)\]/'; - break; - } - if (!empty($pattern)) { - $this->expand_strings('html', $pattern); - } - } - - // captions and messages - - function v6_expand_AlsoCorrect() { - return $this->xml_value("hotpot-config-file,$this->quiztype,also-correct"); - } - function v6_expand_CapitalizeFirst() { - return $this->xml_value("hotpot-config-file,$this->quiztype,capitalize-first-letter"); - } - function v6_expand_CheckCaption() { - return $this->xml_value('hotpot-config-file,global,check-caption'); - } - function v6_expand_CorrectIndicator() { - return $this->xml_value('hotpot-config-file,global,correct-indicator'); - } - function v6_expand_Back() { - return $this->xml_value('hotpot-config-file,global,include-back'); - } - function v6_expand_BackCaption() { - return $this->xml_value('hotpot-config-file,global,back-caption'); - } - function v6_expand_ClickToAdd() { - return $this->xml_value("hotpot-config-file,$this->quiztype,click-to-add"); - } - function v6_expand_Contents() { - return $this->xml_value('hotpot-config-file,global,include-contents'); - } - function v6_expand_ContentsCaption() { - return $this->xml_value('hotpot-config-file,global,contents-caption'); - } - function v6_expand_GuessCorrect() { - return $this->js_value("hotpot-config-file,$this->quiztype,guess-correct"); - } - function v6_expand_GuessIncorrect() { - return $this->js_value("hotpot-config-file,$this->quiztype,guess-incorrect"); - } - function v6_expand_Hint() { - return $this->xml_value("hotpot-config-file,$this->quiztype,include-hint"); - } - function v6_expand_HintCaption() { - return $this->xml_value('hotpot-config-file,global,hint-caption'); - } - function v6_expand_IncorrectIndicator() { - return $this->xml_value('hotpot-config-file,global,incorrect-indicator'); - } - function v6_expand_LastQCaption() { - return $this->xml_value('hotpot-config-file,global,last-q-caption'); - } - function v6_expand_NextCorrect() { - $value = $this->xml_value("hotpot-config-file,$this->quiztype,next-correct-part"); - if (empty($value)) { // jquiz - $value = $this->xml_value("hotpot-config-file,$this->quiztype,next-correct-letter"); - } - return $value; - } - function v6_expand_NextEx() { - return $this->xml_value('hotpot-config-file,global,include-next-ex'); - } - function v6_expand_NextExCaption() { - return $this->xml_value('hotpot-config-file,global,next-ex-caption'); - } - function v6_expand_NextQCaption() { - return $this->xml_value('hotpot-config-file,global,next-q-caption'); - } - function v6_expand_OKCaption() { - return $this->xml_value('hotpot-config-file,global,ok-caption'); - } - function v6_expand_Restart() { - return $this->xml_value("hotpot-config-file,$this->quiztype,include-restart"); - } - function v6_expand_RestartCaption() { - return $this->xml_value('hotpot-config-file,global,restart-caption'); - } - function v6_expand_ShowAllQuestionsCaption() { - return $this->xml_value('hotpot-config-file,global,show-all-questions-caption'); - } - function v6_expand_ShowOneByOneCaption() { - return $this->xml_value('hotpot-config-file,global,show-one-by-one-caption'); - } - function v6_expand_TheseAnswersToo() { - return $this->xml_value("hotpot-config-file,$this->quiztype,also-correct"); - } - function v6_expand_ThisMuch() { - return $this->xml_value("hotpot-config-file,$this->quiztype,this-much-correct"); - } - function v6_expand_Undo() { - return $this->xml_value("hotpot-config-file,$this->quiztype,include-undo"); - } - function v6_expand_UndoCaption() { - return $this->xml_value('hotpot-config-file,global,undo-caption'); - } - function v6_expand_YourScoreIs() { - return $this->xml_value('hotpot-config-file,global,your-score-is'); - } - - // reading - - function v6_expand_Reading() { - return $this->xml_value('data,reading,include-reading'); - } - function v6_expand_ReadingText() { - $title = $this->v6_expand_ReadingTitle(); - $value = $this->xml_value('data,reading,reading-text'); - $value = empty($value) ? '' : ('
'.$value.'
'); - return $title.$value; - } - function v6_expand_ReadingTitle() { - $value = $this->xml_value('data,reading,reading-title'); - return empty($value) ? '' : ('

'.$value.'

'); - } - - // timer - - function v6_expand_Timer() { - return $this->xml_value('data,timer,include-timer'); - } - function v6_expand_JSTimer() { - return $this->read_template('hp6timer.js_'); - } - function v6_expand_Seconds() { - return $this->xml_value('data,timer,seconds'); - } - - // send results - - function v6_expand_SendResults() { - return $this->xml_value("hotpot-config-file,$this->quiztype,send-email"); - } - function v6_expand_JSSendResults() { - return $this->read_template('hp6sendresults.js_'); - } - function v6_expand_FormMailURL() { - return $this->xml_value('hotpot-config-file,global,formmail-url'); - } - function v6_expand_EMail() { - return $this->xml_value('hotpot-config-file,global,email'); - } - function v6_expand_NamePlease() { - return $this->js_value('hotpot-config-file,global,name-please'); - } - - // preload images - - function v6_expand_PreloadImages() { - $value = $this->v6_expand_PreloadImageList(); - return empty($value) ? false : true; - } - function v6_expand_PreloadImageList() { - - // check it has not been set already - if (!isset($this->PreloadImageList)) { - - // the list of image urls - $list = array(); - - // extract tags - $img_tag = htmlspecialchars('|<img.*?src="(.*?)".*?>|is'); - if (preg_match_all($img_tag, $this->source, $matches)) { - $list = $matches[1]; - - // remove duplicates - $list = array_unique($list); + + // read in the XML source and close the file + $this->source = fread($fp, filesize($this->filepath)); + fclose($fp); + + // convert relative URLs to absolute URLs + if ($this->convert_urls) { + $this->hotpot_convert_relative_urls($this->source); } - // convert to comma delimited string - $this->PreloadImageList = empty($list) ? '' : "'".implode(',', $list)."'"; - } - return $this->PreloadImageList; - } - - // html files (all quiz types) - - function v6_expand_PlainTitle() { - return $this->xml_value('data,title'); - } - function v6_expand_ExerciseSubtitle() { - return $this->xml_value("hotpot-config-file,$this->quiztype,exercise-subtitle"); - } - function v6_expand_Instructions() { - return $this->xml_value("hotpot-config-file,$this->quiztype,instructions"); - } - function v6_expand_DublinCoreMetadata() { - return '' - . ''."\n" - . ''."\n" - . ''."\n" - ; - } - function v6_expand_FullVersionInfo() { - global $CFG; - require_once($CFG->hotpotroot.DIRECTORY_SEPARATOR.'version.php'); // set $module - return $this->xml_value('version').'.x (Moodle '.$CFG->release.', hotpot-module '.$this->obj_value($module, 'release').')'; - } - function v6_expand_HeaderCode() { - return $this->xml_value('hotpot-config-file,global,header-code'); - } - function v6_expand_StyleSheet() { - $this->read_template('hp6.cs_', 'css'); - return $this->css; - } - - // stylesheet (hp6.cs_) - - function v6_expand_PageBGColor() { - return $this->xml_value('hotpot-config-file,global,page-bg-color'); - } - function v6_expand_GraphicURL() { - return $this->xml_value('hotpot-config-file,global,graphic-url'); - } - function v6_expand_ExBGColor() { - return $this->xml_value('hotpot-config-file,global,ex-bg-color'); - } - - function v6_expand_FontFace() { - return $this->xml_value('hotpot-config-file,global,font-face'); - } - function v6_expand_FontSize() { - return $this->xml_value('hotpot-config-file,global,font-size'); - } - function v6_expand_TextColor() { - return $this->xml_value('hotpot-config-file,global,text-color'); - } - function v6_expand_TitleColor() { - return $this->xml_value('hotpot-config-file,global,title-color'); - } - function v6_expand_LinkColor() { - return $this->xml_value('hotpot-config-file,global,link-color'); - } - function v6_expand_VLinkColor() { - return $this->xml_value('hotpot-config-file,global,vlink-color'); - } - - function v6_expand_NavTextColor() { - return $this->xml_value('hotpot-config-file,global,page-bg-color'); - } - function v6_expand_NavBarColor() { - return $this->xml_value('hotpot-config-file,global,nav-bar-color'); - } - function v6_expand_NavLightColor() { - $color = $this->xml_value('hotpot-config-file,global,nav-bar-color'); - return $this->get_halfway_color($color, '#ffffff'); - } - function v6_expand_NavShadeColor() { - $color = $this->xml_value('hotpot-config-file,global,nav-bar-color'); - return $this->get_halfway_color($color, '#000000'); - } - - function v6_expand_FuncLightColor() { // top-left of buttons - $color = $this->xml_value('hotpot-config-file,global,ex-bg-color'); - return $this->get_halfway_color($color, '#ffffff'); - } - function v6_expand_FuncShadeColor() { // bottom right of buttons - $color = $this->xml_value('hotpot-config-file,global,ex-bg-color'); - return $this->get_halfway_color($color, '#000000'); - } - - function get_halfway_color($x, $y) { - // returns the $color that is half way between $x and $y - $color = $x; // default - $rgb = '/^\#?([0-9a-f])([0-9a-f])([0-9a-f])$/i'; - $rrggbb = '/^\#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i'; - if (( - preg_match($rgb, $x, $x_matches) || - preg_match($rrggbb, $x, $x_matches) - ) && ( - preg_match($rgb, $y, $y_matches) || - preg_match($rrggbb, $y, $y_matches) - )) { - $color = '#'; - for ($i=1; $i<=3; $i++) { - $x_dec = hexdec($x_matches[$i]); - $y_dec = hexdec($y_matches[$i]); - $color .= sprintf('%02x', min($x_dec, $y_dec) + abs($x_dec-$y_dec)/2); - } - } - return $color; - } - - // navigation buttons - - function v6_expand_NavButtons() { - $back = $this->v6_expand_Back(); - $next_ex = $this->v6_expand_NextEx(); - $contents = $this->v6_expand_Contents(); - return (empty($back) && empty($next_ex) && empty($contents) ? false : true); - } - function v6_expand_NavBarJS() { - return $this->v6_expand_NavButtons(); - } - - // js files (all quiz types) - - function v6_expand_JSBrowserCheck() { - return $this->read_template('hp6browsercheck.js_'); - } - function v6_expand_JSButtons() { - return $this->read_template('hp6buttons.js_'); - } - function v6_expand_JSCard() { - return $this->read_template('hp6card.js_'); - } - function v6_expand_JSCheckShortAnswer() { - return $this->read_template('hp6checkshortanswer.js_'); - } - function v6_expand_JSHotPotNet() { - return $this->read_template('hp6hotpotnet.js_'); - } - function v6_expand_JSShowMessage() { - return $this->read_template('hp6showmessage.js_'); - } - function v6_expand_JSUtilities() { - return $this->read_template('hp6utilities.js_'); - } - - // js files - - function v6_expand_JSJCloze6() { - return $this->read_template('jcloze6.js_'); - } - function v6_expand_JSJCross6() { - return $this->read_template('jcross6.js_'); - } - function v6_expand_JSJMatch6() { - return $this->read_template('jmatch6.js_'); - } - function v6_expand_JSJMix6() { - return $this->read_template('jmix6.js_'); - } - function v6_expand_JSJQuiz6() { - return $this->read_template('jquiz6.js_'); - } - - // drag and drop - - function v6_expand_JSDJMatch6() { - return $this->read_template('djmatch6.js_'); - } - function v6_expand_JSDJMix6() { - return $this->read_template('djmix6.js_'); - } - - // what are these for? - - function v6_expand_JSFJMatch6() { - return $this->read_template('fjmatch6.js_'); - } - function v6_expand_JSFJMix6() { - return $this->read_template('fjmix6.js_'); - } - - // jmatch6.js_ - - function v6_expand_ShuffleQs() { - return $this->bool_value("hotpot-config-file,$this->quiztype,shuffle-questions"); - } - function v6_expand_QsToShow() { - $i = $this->xml_value("hotpot-config-file,$this->quiztype,show-limited-questions"); - if ($i) { - $i = $this->xml_value("hotpot-config-file,$this->quiztype,questions-to-show"); - } - if (empty($i)) { - $i = 0; - switch ($this->quiztype) { - case 'jmatch': - $values = $this->xml_values('data,matching-exercise,pair'); - $i = count($values); - break; - case 'jquiz': - while ($this->xml_value('data,questions,question-record', "[$i]['#']['question'][0]['#']")) { - $i++; - } - break; - } // end switch - } - return $i; - } - function v6_expand_MatchDivItems() { - $str = ''; - - $this->get_jmatch_items($l_items=array(), $r_items = array()); - - $l_keys = $this->shuffle_jmatch_items($l_items); - $r_keys = $this->shuffle_jmatch_items($r_items); - - $options = ''; - foreach ($r_keys as $key) { - $options .= ''."\n"; - } - foreach ($l_keys as $key) { - $str .= '
'; - $str .= ''; - $str .= ''; - } - return $str; - } - - // jmix6.js_ - - function v6_expand_Punctuation() { - $tags = 'data,jumbled-order-exercise'; - $chars = array_merge( - $this->jmix_Punctuation("$tags,main-order,segment"), - $this->jmix_Punctuation("$tags,alternate") - ); - $chars = array_unique($chars); - $chars = implode('', $chars); - $chars = $this->js_safe($chars, true); - return $chars; - } - function jmix_Punctuation($tags) { - $chars = array(); - - // all punctutation except '&#;' (because they are used in html entities) - $ENTITIES = $this->jmix_encode_punctuation('!"$%'."'".'()*+,-./:<=>?@[\]^_`{|}~'); - $pattern = "/&#x([0-9A-F]+);/i"; - $i = 0; - - // get next segment (or alternate answer) - while ($value = $this->xml_value($tags, "[$i]['#']")) { - - // convert low-ascii punctuation to entities - $value = strtr($value, $ENTITIES); - - // extract all hex HTML entities - if (preg_match_all($pattern, $value, $matches)) { - - // loop through hex entities - $m_max = count($matches[0]); - for ($m=0; $m<$m_max; $m++) { - - // convert to hex number - eval('$hex=0x'.$matches[1][$m].';'); - - // is this a punctuation character? - if ( - ($hex>=0x0020 && $hex<=0x00BF) || // ascii punctuation - ($hex>=0x2000 && $hex<=0x206F) || // general punctuation - ($hex>=0x3000 && $hex<=0x303F) || // CJK punctuation - ($hex>=0xFE30 && $hex<=0xFE4F) || // CJK compatability - ($hex>=0xFE50 && $hex<=0xFE6F) || // small form variants - ($hex>=0xFF00 && $hex<=0xFF40) || // halfwidth and fullwidth forms (1) - ($hex>=0xFF5B && $hex<=0xFF65) || // halfwidth and fullwidth forms (2) - ($hex>=0xFFE0 && $hex<=0xFFEE) // halfwidth and fullwidth forms (3) - ) { - // add this character - $chars[] = $matches[0][$m]; - } - } - } - $i++; - } - - return $chars; - } - function v6_expand_OpenPunctuation() { - $tags = 'data,jumbled-order-exercise'; - $chars = array_merge( - $this->jmix_OpenPunctuation("$tags,main-order,segment"), - $this->jmix_OpenPunctuation("$tags,alternate") - ); - $chars = array_unique($chars); - $chars = implode('', $chars); - $chars = $this->js_safe($chars, true); - return $chars; - } - function jmix_OpenPunctuation($tags) { - $chars = array(); - - // unicode punctuation designations (pi="initial quote", ps="open") - // http://www.sql-und-xml.de/unicode-database/pi.html - // http://www.sql-und-xml.de/unicode-database/ps.html - $pi = '0022|0027|00AB|2018|201B|201C|201F|2039'; - $ps = '0028|005B|007B|0F3A|0F3C|169B|201A|201E|2045|207D|208D|2329|23B4|2768|276A|276C|276E|2770|2772|2774|27E6|27E8|27EA|2983|2985|2987|2989|298B|298D|298F|2991|2993|2995|2997|29D8|29DA|29FC|3008|300A|300C|300E|3010|3014|3016|3018|301A|301D|FD3E|FE35|FE37|FE39|FE3B|FE3D|FE3F|FE41|FE43|FE47|FE59|FE5B|FE5D|FF08|FF3B|FF5B|FF5F|FF62'; - $pattern = "/(&#x($pi|$ps);)/i"; - - $ENTITIES = $this->jmix_encode_punctuation('"'."'".'(<[{'); - - $i = 0; - while ($value = $this->xml_value($tags, "[$i]['#']")) { - $value = strtr($value, $ENTITIES); - if (preg_match_all($pattern, $value, $matches)) { - $chars = array_merge($chars, $matches[0]); - } - $i++; - } - - return $chars; - } - function jmix_encode_punctuation($str) { - $ENTITIES = array(); - $i_max = strlen($str); - for ($i=0; $i<$i_max; $i++) { - $ENTITIES[$str{$i}] = '&#x'.sprintf('%04X', ord($str{$i})).';'; - } - return $ENTITIES; - } - function v6_expand_ExerciseTitle() { - return $this->xml_value('data,title'); - } - - // Jmix specials - - function v6_expand_SegmentArray() { - $segments = $this->xml_values('data,jumbled-order-exercise,main-order,segment'); - - $this->seed_random_number_generator(); - $keys = array_keys($segments); - shuffle($keys); - - $str = ''; - for($i=0; $ijs_safe($segments[$keys[$i]])."';\n"; - $str .= "Segments[$i][1] = ".($keys[$i]+1).";\n"; - $str .= "Segments[$i][2] = 0;\n"; - } - return $str; - } - function v6_expand_AnswerArray() { - - $segments = $this->xml_values('data,jumbled-order-exercise,main-order,segment'); - $alternates = $this->xml_values('data,jumbled-order-exercise,alternate'); - - $i = 0; - $pattern = ''; - $str = 'Answers['.$i++.'] = new Array('; - for($ii=0; $iixml_value("hotpot-config-file,$this->quiztype,remaining-words"); - } - function v6_expand_TimesUp() { - return $this->xml_value('hotpot-config-file,global,times-up'); - } - - // nav bar - - function v6_expand_NavBar() { - $tag = 'navbar'; - $this->read_template('hp6navbar.ht_', $tag); - return $this->$tag; - } - function v6_expand_TopNavBar() { - return $this->v6_expand_NavBar(); - } - function v6_expand_BottomNavBar() { - return $this->v6_expand_NavBar(); - } - function v6_expand_NextExURL() { - return $this->xml_value("hotpot-config-file,$this->quiztype,next-ex-url"); - } - - // hp6navbar.ht_ - - function v6_expand_NavBarID() { - return ''; // what's this?; - } - function v6_expand_ContentsURL() { - return $this->xml_value('hotpot-config-file,global,contents-url'); - } - - // conditional blocks - - function v6_expand_ShowAnswer() { - return $this->xml_value("hotpot-config-file,$this->quiztype,include-show-answer"); - } - function v6_expand_Slide() { - return true; // whats's this (JMatch drag and drop) - } - - // specials (JMatch) - - function v6_expand_FixedArray() { - $str = ''; - $this->get_jmatch_items($l_items=array(), $r_items = array()); - foreach ($l_items as $i=>$item) { - $str .= "F[$i] = new Array();\n"; - $str .= "F[$i][0] = '".$this->js_safe($item['text'][0]['#'], true)."';\n"; - $str .= "F[$i][1] = ".($i+1).";\n"; - } - return $str; - } - function v6_expand_DragArray() { - $str = ''; - $this->get_jmatch_items($l_items=array(), $r_items = array()); - foreach ($r_items as $i=>$item) { - $str .= "D[$i] = new Array();\n"; - $str .= "D[$i][0] = '".$this->js_safe($item['text'][0]['#'], true)."';\n"; - $str .= "D[$i][1] = ".($i+1).";\n"; - $str .= "D[$i][2] = 0;\n"; - } - return $str; - } - - function get_jmatch_items(&$l_items, &$r_items) { - $i = 0; - while( - ($l_item = $this->xml_value('data,matching-exercise,pair',"[$i]['#']['left-item'][0]['#']")) && - ($r_item = $this->xml_value('data,matching-exercise,pair',"[$i]['#']['right-item'][0]['#']")) - ) { - $l_items[] = $l_item; - $r_items[] = $r_item; - $i++; - } - } - function shuffle_jmatch_items(&$items) { - // get moveable items - $moveable_keys = array(); - for($i=0; $iseed_random_number_generator(); - shuffle($moveable_keys); - - $keys = array(); - for($i=0, $ii=0; $iquiztype) { - case 'jcloze': - $str .= "I = new Array();\n"; - $tags = 'data,gap-fill,question-record'; - while (($question="[$q]['#']") && $this->xml_value($tags, $question)) { - $a = 0; - $aa = 0; - while (($answer=$question."['answer'][$a]['#']") && $this->xml_value($tags, $answer)) { - $text = $this->js_value($tags, $answer."['text'][0]['#']", true); - if ($text) { - if ($aa==0) { // first time only - $str .= "I[$q] = new Array();\n"; - $str .= "I[$q][1] = new Array();\n"; - } - $str .= "I[$q][1][$aa] = new Array();\n"; - $str .= "I[$q][1][$aa][0] = '$text';\n"; - $aa++; - } - $a++; - } - // add clue, if any answers were found - if ($aa) { - $clue = $this->js_value($tags, $question."['clue'][0]['#']", true); - $str .= "I[$q][2]='$clue';\n"; - } - $q++; - } - break; - case 'jquiz': - $str .= "I=new Array();\n"; - $tags = 'data,questions,question-record'; - while (($question="[$q]['#']") && $this->xml_value($tags, $question)) { - - $question_type = $this->int_value($tags, $question."['question-type'][0]['#']"); - $weighting = $this->int_value($tags, $question."['weighting'][0]['#']"); - $clue = $this->js_value($tags, $question."['clue'][0]['#']", true); - - $answers = $question."['answers'][0]['#']"; - - $a = 0; - $aa = 0; - while (($answer = $answers."['answer'][$a]['#']") && $this->xml_value($tags, $answer)) { - $text = $this->js_value($tags, $answer."['text'][0]['#']", true); - $feedback = $this->js_value($tags, $answer."['feedback'][0]['#']", true); - $correct = $this->int_value($tags, $answer."['correct'][0]['#']", true); - $percent = $this->int_value($tags, $answer."['percent-correct'][0]['#']", true); - $include = $this->int_value($tags, $answer."['include-in-mc-options'][0]['#']", true); - if ($text) { - if ($aa==0) { // first time only - $str .= "I[$q]=new Array();\n"; - $str .= "I[$q][0]=$weighting;\n"; - $str .= "I[$q][1]='$clue';\n"; - $str .= "I[$q][2]='".($question_type-1)."';\n"; - $str .= "I[$q][3]=new Array();\n"; - } - $str .= "I[$q][3][$aa]=new Array('$text','$feedback',$correct,$percent,$include);\n"; - $aa++; - } - $a++; - } - $q++; - } - break; - } - return $str; - } - - function v6_expand_ClozeBody() { - $str = ''; - $q = 0; - $tags = 'data,gap-fill'; - while ($text = $this->xml_value($tags, "[0]['#'][$q]")) { - $str .= $text; - if (($question="[$q]['#']") && $this->xml_value("$tags,question-record", $question)) { - $str .= ''; - } - $q++; - } - - - return $str; - } - - // JCloze quiztype - - function v6_expand_WordList() { - return $this->xml_value("hotpot-config-file,$this->quiztype,include-word-list"); - } - function v6_expand_Keypad() { - $str = ''; - if ($this->bool_value("hotpot-config-file,$this->quiztype,include-keypad")) { - - // these characters must always be in the keypad - $chars = array(); - $this->add_keypad_chars($chars, $this->xml_value('hotpot-config-file,global,keypad-characters')); + if ($this->parse_xml) { - // append other characters used in the answers - $tags = ''; - switch ($this->quiztype) { - case 'jcloze': - $tags = 'data,gap-fill,question-record'; - break; - case 'jquiz': - $tags = 'data,questions,question-record'; - break; - } - if ($tags) { - $q = 0; - while (($question="[$q]['#']") && $this->xml_value($tags, $question)) { - - if ($this->quiztype=='jquiz') { - $answers = $question."['answers'][0]['#']"; - } else { - $answers = $question; + // prepend initial tag if required (JCloze HP5) + if (preg_match('|\.html?$|', $this->filename)) { + if (preg_match('|\s*$|i', $this->source) && !preg_match('|^\s*|i', $this->source)) { + $this->source = ''.$this->source; } - - $a = 0; - while (($answer=$answers."['answer'][$a]['#']") && $this->xml_value($tags, $answer)) { - $this->add_keypad_chars($chars, $this->xml_value($tags, $answer."['text'][0]['#']")); - $a++; - } - $q++; } - } + + // encode "gap fill" text in JCloze exercise + $this->encode_cdata($this->source, 'gap-fill'); + + // convert source to xml tree + $this->hotpot_xml_tree($this->source); - // remove duplicate characters and sort - $chars = array_unique($chars); - usort($chars, "hotpot_sort_keypad_chars"); + // initialize file type, quiz type and output format + $this->html = ''; + $this->filetype = ''; + $this->quiztype = ''; + $this->outputformat = 0; // undefined + + // link tag to , if necessary + if (isset($this->xml['HTML'])) { + $this->xml['html'] = &$this->xml['HTML']; + } + + if (isset($this->xml['html'])) { + + $this->filetype = 'html'; + $this->quiztype = ''; + + // relative URLs in "PreloadImages(...);" + $search = '%'.'(?<='.'PreloadImages'.'\('.')'."([^)]+?)".'(?='.'\);'.')'.'%se'; + $replace = "hotpot_convert_preloadimages_urls('".$this->get_baseurl()."','".$this->reference."','\\1')"; + $this->source = preg_replace($search, $replace, $this->source); - // create keypad buttons for each character - $str .= '
'; - foreach ($chars as $char) { - $str .= ""; - } - $str .= '
'; - } - return $str; - } - function add_keypad_chars(&$chars, $text) { - if (preg_match_all('|&[^;]+;|i', $text, $more_chars)) { - $chars = array_merge($chars, $more_chars[0]); - } - } - function v6_expand_Correct() { - return $this->xml_value("hotpot-config-file,$this->quiztype,guesses-correct"); - } - function v6_expand_Incorrect() { - return $this->xml_value("hotpot-config-file,$this->quiztype,guesses-incorrect"); - } - function v6_expand_GiveHint() { - return $this->xml_value("hotpot-config-file,$this->quiztype,next-correct-letter"); - } - function v6_expand_CaseSensitive() { - return $this->xml_value("hotpot-config-file,$this->quiztype,case-sensitive"); - } - - // JCross quiztype - - function v6_expand_CluesAcrossLabel() { - return $this->xml_value("hotpot-config-file,$this->quiztype,clues-across"); - } - function v6_expand_CluesDownLabel() { - $this->xml_value("hotpot-config-file,$this->quiztype,clues-down"); - return ''; - } - function v6_expand_EnterCaption() { - return $this->xml_value("hotpot-config-file,$this->quiztype,enter-caption"); - } - function v6_expand_ShowHideClueList() { - $value = $this->xml_value("hotpot-config-file,$this->quiztype,include-clue-list"); - return empty($value) ? ' style="display: none;"' : ''; - } - - // JCross specials - - function v6_expand_CluesDown() { - return $this->v6_expand_jcross_clues('D'); - } - function v6_expand_CluesAcross() { - return $this->v6_expand_jcross_clues('A'); - } - function v6_expand_jcross_clues($direction) { - // $direction: A(cross) or D(own) - $this->v6_get_jcross_grid($row=NULL, $r_max=0, $c_max=0); - $i = 0; // clue index; - $str = ''; - for($r=0; $r<=$r_max; $r++) { - for($c=0; $c<=$c_max; $c++) { - $aword = $this->get_jcross_aword($row, $r, $r_max, $c, $c_max); - $dword = $this->get_jcross_dword($row, $r, $r_max, $c, $c_max); - if ($aword || $dword) { - $i++; // increment clue index - - // get the definition for this word - $def = ''; - $word = ($direction=='A') ? $aword : $dword; - $clues = $this->xml_values('data,crossword,clues,item'); - foreach ($clues as $clue) { - if ($clue['word'][0]['#']==$word) { - $def = $clue['def'][0]['#']; - $def = strtr($def, array('<'=>'<', '>'=>'>', "\n"=>'
')); + } else { + $this->filetype = 'xml'; + + $keys = array_keys($this->xml); + foreach ($keys as $key) { + if (preg_match('/^(hotpot|textoys)-(\w+)-file$/i', $key, $matches)) { + $this->quiztype = strtolower($matches[2]); + $this->xml_root = "['$key']['#']"; break; } } + } - if (!empty($def)) { - $str .= '
'; + if ($this->create_html) { + + // set the real output format from the requested output format + $this->real_outputformat = $this->obj_value($obj, 'outputformat'); + $this->draganddrop = ''; + if ( + empty($this->real_outputformat) || + $this->real_outputformat==HOTPOT_OUTPUTFORMAT_BEST || + empty($HOTPOT_OUTPUTFORMAT_DIR[$this->real_outputformat]) + ) { + // set the best output format for this browser + // see http://jp2.php.net/function.get-browser + if (function_exists('get_browser') && ini_get('browscap')) { + $b = get_browser(); + // apparently get_browser is a slow + // so we should store the results in $this->browser + } else { + $ua = $_SERVER['HTTP_USER_AGENT']; + $b = NULL; + // store the results in $this->browser + // [parent] => Firefox 0.9 + // [platform] => WinXP + // [browser] => Firefox + // [version] => 0.9 + // [majorver] => 0 + // [minorver] => 9 + } + if ($this->quiztype=='jmatch' || $this->quiztype=='jmix') { + $this->real_outputformat = HOTPOT_OUTPUTFORMAT_V6_PLUS; + } else { + $this->real_outputformat = HOTPOT_OUTPUTFORMAT_V6; + } } - } - } - } - return $str; - } - - // jcross6.js_ - - function v6_expand_LetterArray() { - $this->v6_get_jcross_grid($row=NULL, $r_max=0, $c_max=0); - $str = ''; - for($r=0; $r<=$r_max; $r++) { - $str .= "L[$r] = new Array("; - for($c=0; $c<=$c_max; $c++) { - $str .= ($c>0 ? ',' : '')."'".$this->js_safe($row[$r]['cell'][$c]['#'], true)."'"; - } - $str .= ");\n"; - } - return $str; - } - function v6_expand_GuessArray() { - $this->v6_get_jcross_grid($row=NULL, $r_max=0, $c_max=0); - $str = ''; - for($r=0; $r<=$r_max; $r++) { - $str .= "G[$r] = new Array('".str_repeat("','", $c_max)."');\n"; - } - return $str; - } - function v6_expand_ClueNumArray() { - $this->v6_get_jcross_grid($row=NULL, $r_max=0, $c_max=0); - $i = 0; // clue index - $str = ''; - for($r=0; $r<=$r_max; $r++) { - $str .= "CL[$r] = new Array("; - for($c=0; $c<=$c_max; $c++) { - if ($c>0) { - $str .= ','; - } - $aword = $this->get_jcross_aword($row, $r, $r_max, $c, $c_max); - $dword = $this->get_jcross_dword($row, $r, $r_max, $c, $c_max); - if (empty($aword) && empty($dword)) { - $str .= 0; - } else { - $i++; // increment the clue index - $str .= $i; - } - } - $str .= ");\n"; - } - return $str; - } - function v6_expand_GridBody() { - $this->v6_get_jcross_grid($row=NULL, $r_max=0, $c_max=0); - $i = 0; // clue index; - $str = ''; - for($r=0; $r<=$r_max; $r++) { - $str .= ''; - for($c=0; $c<=$c_max; $c++) { - if (empty($row[$r]['cell'][$c]['#'])) { - $str .= ''; - } else { - $aword = $this->get_jcross_aword($row, $r, $r_max, $c, $c_max); - $dword = $this->get_jcross_dword($row, $r, $r_max, $c, $c_max); - if (empty($aword) && empty($dword)) { - $str .= ''; + + if ($this->real_outputformat==HOTPOT_OUTPUTFORMAT_V6_PLUS) { + if ($this->quiztype=='jmatch' || $this->quiztype=='jmix') { + $this->draganddrop = 'd'; // prefix for templates (can also be "f" ?) + } + $this->real_outputformat = HOTPOT_OUTPUTFORMAT_V6; + } + + // set the output html + $this->html = ''; + if ($this->filetype=='html') { + $this->html = &$this->source; + } else { - $i++; // increment clue index - $str .= ''; - } - } - } - $str .= ''; - } - return $str; - } - function v6_get_jcross_grid(&$row, &$r_max, &$c_max) { - $row = $this->xml_values('data,crossword,grid,row'); - $r_max = 0; - $c_max = 0; - if (isset($row) && is_array($row)) { - for($r=0; $rget_jcross_word($row, $r, $r_max, $c, $c_max, true); - } - return $str; - } - function get_jcross_aword(&$row, $r, $r_max, $c, $c_max) { - $str = ''; - if (($c==0 || empty($row[$r]['cell'][$c-1]['#'])) && $c<$c_max && !empty($row[$r]['cell'][$c+1]['#'])) { - $str = $this->get_jcross_word($row, $r, $r_max, $c, $c_max, false); - } - return $str; - } - function get_jcross_word(&$row, $r, $r_max, $c, $c_max, $go_down=false) { - $str = ''; - while ($r<=$r_max && $c<=$c_max && !empty($row[$r]['cell'][$c]['#'])) { - $str .= $row[$r]['cell'][$c]['#']; - if ($go_down) { - $r++; - } else { - $c++; - } - } - return $str; - } - - // specials (JQuiz) - - function v6_expand_QuestionOutput() { - $str = ''; - $str .= '
    '."\n"; - - $q = 0; - $tags = 'data,questions,question-record'; - while (($question="[$q]['#']") && $this->xml_value($tags, $question)) { - - // get question - $question_text = $this->xml_value($tags, $question."['question'][0]['#']"); - $question_type = $this->xml_value($tags, $question."['question-type'][0]['#']"); - - // check we have a question - if ($question_text && $question_type) { - - $str .= '
'.$l_items[$key]['text'][0]['#'].'
'.$i.'. '.$def.'
  '.$i.'   
- + @@ -154,10 +189,13 @@ $yes_no_options = array( ?> - + @@ -167,6 +205,21 @@ $yes_no_options = array( helpbutton("outputformat", get_string("outputformat","hotpot"), "hotpot"); ?> + + + + + + + + + + + + + + +
: - - mode=='add') { + choose_from_menu($text_source_options, "namesource", "$form->namesource", ""); + } else { + print ''; + } + print ''; + print ''; + print ''; + ?>
: + ?>:
'; @@ -43,40 +55,63 @@ $yes_no_options = array( echo '
'; emoticonhelpbutton("form", "description"); echo '
'; - } + } ?>
mode=='add') { + choose_from_menu($text_source_options, "summarysource", "$form->summarysource", ""); + } else { + print ''; + } + print ''; if (function_exists("print_textarea") && isset($usehtmleditor)) { print_textarea($usehtmleditor, 10, 65, 680, 400, "summary", $form->summary); } else { // Moodle 1.1.1 (original size was rows="5" cols="50") print ''; } + print ''; ?>
: timeopen and $course->format == "weeks") { + $options = array( + HOTPOT_NO => get_string("alwaysopen", "hotpot"), + HOTPOT_YES => get_string("specifictime", "hotpot") + ); + choose_from_menu($options, "enabletimeopen", "$form->enabletimeopen", ""); + print ''; + print '   '; + if (!$form->timeopen && $course->format == "weeks") { $form->timeopen= $course->startdate + (($form->section - 1) * 608400); } print_date_selector("openday", "openmonth", "openyear", $form->timeopen); print ' - '; print_time_selector("openhour", "openminute", $form->timeopen); helpbutton("timeopen", get_string("quizopen","quiz"), "quiz"); + print ''; ?>
: timeclose and $course->format == "weeks") { + $options = array( + HOTPOT_NO => get_string("neverclosed", "hotpot"), + HOTPOT_YES => get_string("specifictime", "hotpot") + ); + choose_from_menu($options, "enabletimeclose", "$form->enabletimeclose", ""); + print ''; + print '   '; + if (!$form->timeclose && $course->format == "weeks") { $form->timeclose= $course->startdate + (($form->section) * 608400); } print_date_selector("closeday", "closemonth", "closeyear", $form->timeclose); print ' - '; print_time_selector("closehour", "closeminute", $form->timeclose); helpbutton("timeopen", get_string("quizclose","quiz"), "quiz"); + print ''; ?>
:mode}quizchain"; + print_string($quizchain, "hotpot"); + ?>: navigation", ""); - helpbutton("navigation", get_string("navigation","hotpot"), "hotpot"); + choose_from_menu($yes_no_options, "quizchain", $form->quizchain, ""); + helpbutton($quizchain, get_string($quizchain,"hotpot"), "hotpot"); ?>
:navigation", ""); + helpbutton("navigation", get_string("navigation","hotpot"), "hotpot"); + ?>
:studentfeedback", ""); + print "studentfeedbackurl\">"; + helpbutton("studentfeedback", get_string("studentfeedback","hotpot"), "hotpot"); + ?>
:
:clickreporting", ""); + helpbutton("clickreporting", get_string("clickreporting","hotpot"), "hotpot"); + ?>
  + + ">     + " />
+ release) && substr($CFG->release, 0, 3)>=1.5) { + $options = array( + HOTPOT_DISPLAYNEXT_QUIZ => get_string("displayhotpotnext", "hotpot"), + HOTPOT_DISPLAYNEXT_COURSE => get_string("displaycoursenext", "hotpot"), + HOTPOT_DISPLAYNEXT_INDEX => get_string("displayindexnext", "hotpot") + ); + choose_from_menu($options, "displaynext", "$form->displaynext", ""); + } else { + print ''."\n"; + } + ?>
@@ -249,13 +327,85 @@ $yes_no_options = array( sesskey)) { ?> - - -">   -" /> - + + + timeopen) ? HOTPOT_NO : HOTPOT_YES); + set_form_field($form, 'timeclose', get_user_preferences('hotpot_timeclose', 0)); + set_form_field($form, 'enabletimeclose', empty($form->timeclose) ? HOTPOT_NO : HOTPOT_YES); set_form_field($form, 'location', HOTPOT_LOCATION_COURSEFILES); set_form_field($form, 'reference'); - set_form_field($form, 'navigation', HOTPOT_NAVIGATION_BAR); - set_form_field($form, 'outputformat', HOTPOT_OUTPUTFORMAT_BEST); - set_form_field($form, 'forceplugins', HOTPOT_NO); - set_form_field($form, 'shownextquiz', HOTPOT_NO); - set_form_field($form, 'review', HOTPOT_YES); - set_form_field($form, 'grade', 100); - set_form_field($form, 'grademethod', HOTPOT_GRADEMETHOD_HIGHEST); - set_form_field($form, 'attempts', 0); // unlimited + set_form_field($form, 'navigation', get_user_preferences('hotpot_navigation', HOTPOT_NAVIGATION_BAR)); + set_form_field($form, 'outputformat', get_user_preferences('hotpot_outputformat', HOTPOT_OUTPUTFORMAT_BEST)); + set_form_field($form, 'studentfeedback', get_user_preferences('hotpot_studentfeedback', HOTPOT_FEEDBACK_NONE)); + set_form_field($form, 'studentfeedbackurl', get_user_preferences('hotpot_studentfeedbackurl', 'http://')); + set_form_field($form, 'forceplugins', get_user_preferences('hotpot_forceplugins', HOTPOT_NO)); + if ($form->mode=='add') { + set_form_field($form, 'namesource', get_user_preferences('hotpot_namesource', HOTPOT_TEXTSOURCE_QUIZ)); + set_form_field($form, 'summarysource', get_user_preferences('hotpot_summarysource', HOTPOT_TEXTSOURCE_QUIZ)); + $quizchain = get_user_preferences('hotpot_quizchain', HOTPOT_NO); + } else { + $quizchain = empty($form->quizchain) ? HOTPOT_NO : HOTPOT_YES; + } + set_form_field($form, 'quizchain', $quizchain); + set_form_field($form, 'shownextquiz', get_user_preferences('hotpot_shownextquiz', HOTPOT_NO)); + set_form_field($form, 'review', get_user_preferences('hotpot_review', HOTPOT_YES)); + set_form_field($form, 'grade', get_user_preferences('hotpot_grade', 100)); + set_form_field($form, 'grademethod', get_user_preferences('hotpot_grademethod', HOTPOT_GRADEMETHOD_HIGHEST)); + set_form_field($form, 'attempts', get_user_preferences('hotpot_attempts', 0)); // 0=unlimited set_form_field($form, 'password'); - set_form_field($form, 'subnet'); + set_form_field($form, 'subnet', get_user_preferences('hotpot_subnet')); + set_form_field($form, 'clickreporting', HOTPOT_NO); + set_form_field($form, 'displaynext', get_user_preferences('hotpot_displaynext', HOTPOT_DISPLAYNEXT_QUIZ)); } function set_form_field(&$form, $fieldname, $defaultvalue='') { if (!isset($form->$fieldname)) { diff --git a/mod/hotpot/report.php b/mod/hotpot/report.php index 6778cf1fd4c..8ee9bbca046 100644 --- a/mod/hotpot/report.php +++ b/mod/hotpot/report.php @@ -38,40 +38,37 @@ // get report mode if (isteacher($course->id)) { - $mode = optional_param("mode", "overview", 0); - if (is_array($mode)) { - $mode = array_keys($mode); - $mode = $mode[0]; - } + $mode = optional_param("mode", "overview"); } else { // students have no choice $mode = 'overview'; } + + // assemble array of form data + $formdata = array( + 'mode' => $mode, + 'reportcourse' => isadmin() ? optional_param('reportcourse', get_user_preferences('hotpot_reportcourse', 'this')) : 'this', + 'reportusers' => isteacher($course->id) ? optional_param('reportusers', get_user_preferences('hotpot_reportusers', 'all')) : 'this', + 'reportattempts' => optional_param('reportattempts', get_user_preferences('hotpot_reportattempts', 'all')), + 'reportformat' => optional_param('reportformat', 'htm'), + 'reportshowlegend' => optional_param('reportshowlegend', get_user_preferences('hotpot_reportshowlegend', '0')), + 'reportencoding' => optional_param('reportencoding', get_user_preferences('hotpot_reportencoding', '')), + 'reportwrapdata' => optional_param('reportwrapdata', get_user_preferences('hotpot_reportwrapdata', '1')), + ); - // get report attributes - if (isadmin()) { - $reportcourse = optional_param("reportcourse", "this"); - } else { - // students and ordinary teachers have no choice - $reportcourse = 'this'; + foreach ($formdata as $name=>$value) { + set_user_preference("hotpot_$name", $value); } - if (isteacher($course->id)) { - $reportusers = optional_param("reportusers", "all"); - } else { - // students have no choice - $reportusers = 'this'; - } - $reportattempts = optional_param("reportattempts", "all"); /// Start the report - add_to_log($course->id, "hotpot", "report", "report.php?id=$cm->id", "$hotpot->id", "$cm->id"); + add_to_log($course->id, "hotpot", "report", "report.php?id=$cm->id&mode=$mode", "$hotpot->id", "$cm->id"); // print page header. if required - if (empty($noheader)) { + if ($formdata['reportformat']=='htm') { hotpot_print_report_heading($course, $cm, $hotpot, $mode); if (isteacher($course->id)) { - hotpot_print_report_selector($course, $hotpot, $mode, $reportcourse, $reportusers, $reportattempts); + hotpot_print_report_selector($course, $hotpot, $formdata); } } @@ -83,7 +80,7 @@ $hotpot_ids = ''; $course_ids = ''; - switch ($reportcourse) { + switch ($formdata['reportcourse']) { case 'this': $course_ids = $course->id; $hotpot_ids = $hotpot->id; @@ -99,7 +96,7 @@ $user_ids = ''; $users = array(); - switch ($reportusers) { + switch ($formdata['reportusers']) { case 'all': $admin_ids = get_records_select_menu('user_admins'); if (is_array($admin_ids)) { @@ -122,7 +119,9 @@ if (is_array($student_ids)) { $users = array_merge($users, $student_ids); } - $user_ids = join(',', array_values($users)); + $user_ids = array_values($users); + sort($user_ids); + $user_ids = join(',', array_unique($user_ids)); break; case 'this': $user_ids = $USER->id; @@ -134,18 +133,79 @@ exit; } - // get attempts for these users - $fields = 'a.*, u.firstname, u.lastname, u.picture'; - $table = "{$CFG->prefix}hotpot_attempts AS a, {$CFG->prefix}user AS u"; - $select = ($mode=='simplestat' || $mode=='fullstat') ? 'a.score IS NOT NULL' : 'a.timefinish>0'; - $select .= " AND a.hotpot IN ($hotpot_ids) AND a.userid IN ($user_ids) AND a.userid = u.id"; - $order = "u.lastname, a.attempt"; - if ($reportattempts=='best') { - $fields .= ", MAX(a.score) AS grade"; - $select .= " GROUP BY a.userid"; + // database table and selection conditions + $table = "{$CFG->prefix}hotpot_attempts AS a"; + $select = "a.hotpot IN ($hotpot_ids) AND a.userid IN ($user_ids)"; + if ($mode!='overview') { + $select .= ' AND a.status<>'.HOTPOT_STATUS_INPROGRESS; } - $attempts = get_records_sql("SELECT $fields FROM $table WHERE $select ORDER BY $order"); + // confine attempts if necessary + switch ($formdata['reportattempts']) { + case 'best': + $function = 'MAX'; + $fieldnames = array('score', 'id', 'clickreportid'); + break; + case 'first': + $function = 'MIN'; + $fieldnames = array('timestart', 'id', 'clickreportid'); + break; + case 'last': + $function = 'MAX'; + $fieldnames = array('timestart', 'id', 'clickreportid'); + break; + default: // 'all' and any others + $function = ''; + $fieldnames = array(); + break; + } + if (empty($function) || empty($fieldnames)) { + // do nothing (i.e. get ALL attempts) + } else { + $groupby = 'userid'; + $records = hotpot_get_records_groupby($function, $fieldnames, $table, $select, $groupby); + + $ids = array(); + foreach ($records as $record) { + $ids[] = $record->clickreportid; + } + $select = "a.clickreportid IN (".join(',', $ids).")"; + } + + // pick out last attempt in each clickreport series + $cr_attempts = hotpot_get_records_groupby('MAX', array('timefinish', 'id'), $table, $select, 'clickreportid'); + + $fields = 'a.*, u.firstname, u.lastname, u.picture'; + if ($mode=='click') { + $fields .= ', u.idnumber'; + } else { + // overview, simple and detailed reports + // get last attempt record in clickreport series + $ids = array(); + foreach ($cr_attempts as $cr_attempt) { + $ids[] = $cr_attempt->id; + } + if (empty($ids)) { + $select = ""; + } else { + $ids = array_unique($ids); + sort($ids); + $select = "a.id IN (".join(',', $ids).")"; + } + } + + $attempts = array(); + + if ($select) { + // add user information to SQL query + $select .= ' AND a.userid = u.id'; + $table .= ", {$CFG->prefix}user AS u"; + $order = "u.lastname, a.attempt, a.timefinish"; + // get the attempts (at last!) + $attempts = get_records_sql("SELECT $fields FROM $table WHERE $select ORDER BY $order"); + } + + // stop now if no attempts were found if (empty($attempts)) { print_heading(get_string('noattempts','quiz')); exit; @@ -159,22 +219,26 @@ // get grades $grades = hotpot_get_grades($hotpot, $user_ids); - // get list of attempts by user + // get list of attempts by user and set reference to last attempt in clickreport series $users = array(); foreach ($attempts as $id=>$attempt) { $userid = $attempt->userid; if (!isset($users[$userid])) { - $grade = isset($grades[$userid]) ? $grades[$userid] : ' '; - $users[$userid]->grade = $grade; + $users[$userid]->grade = isset($grades[$userid]) ? $grades[$userid] : ' '; $users[$userid]->attempts = array(); } $users[$userid]->attempts[] = &$attempts[$id]; - + + if ($mode=='click' && isset($cr_attempts[$id])) { + $attempts[$id]->cr_lastclick = $cr_attempts[$id]->id; + $attempts[$id]->cr_timefinish = $cr_attempts[$id]->timefinish; + } } + /// Open the selected hotpot report and display it $mode = clean_filename($mode); @@ -188,18 +252,17 @@ $report = new hotpot_report(); - if (! $report->display($hotpot, $cm, $course, $users, $attempts, $questions)) { - error("Error occurred during pre-processing!", $course_homeurl); + if (! $report->display($hotpot, $cm, $course, $users, $attempts, $questions, $formdata)) { + error("Error occurred during report processing!", $course_homeurl); } - if (empty($noheader)) { + if ($formdata['reportformat']=='htm') { print_footer($course); } - ////////////////////////////////////////////// /// functions to delete attempts and responses -function hotpot_grade_heading($hotpot, $download) { +function hotpot_grade_heading($hotpot, $formdata) { global $HOTPOT_GRADEMETHOD; $grademethod = $HOTPOT_GRADEMETHOD[$hotpot->grademethod]; @@ -207,10 +270,10 @@ function hotpot_grade_heading($hotpot, $download) { if ($hotpot->grade!=100) { $grademethod = "$hotpot->grade x $grademethod/100"; } - if (empty($download)) { + if ($formdata['reportformat']=='htm') { $grademethod = ''.$grademethod.''; } - $nl = $download ? "\n" : '
'; + $nl = $formdata['reportformat']=='htm' ? '
' : "\n"; return get_string('grade')."$nl($grademethod)"; } function hotpot_delete_selected_attempts(&$hotpot, $del) { @@ -221,14 +284,14 @@ function hotpot_delete_selected_attempts(&$hotpot, $del) { $select = "hotpot='$hotpot->id'"; break; case 'abandoned': - $select = "hotpot='$hotpot->id' AND timefinish>0 AND score IS NULL"; + $select = "hotpot='$hotpot->id' AND status=".HOTPOT_STATUS_ABANDONED; break; case 'selection': $ids = (array)data_submitted(); unset($ids['del']); unset($ids['id']); if (!empty($ids)) { - $select = "hotpot='$hotpot->id' AND id IN (".implode(',', $ids).")"; + $select = "hotpot='$hotpot->id' AND clickreportid IN (".implode(',', $ids).")"; } break; } @@ -241,9 +304,9 @@ function hotpot_delete_selected_attempts(&$hotpot, $del) { hotpot_delete_and_notify($table, $select, get_string('attempts', 'quiz')); - $table = 'hotpot_responses'; $select = 'attempt IN ('.implode(',', array_keys($attempts)).')'; - hotpot_delete_and_notify($table, $select, get_string('answer', 'quiz')); + hotpot_delete_and_notify('hotpot_details', $select, get_string('rawdetails', 'hotpot')); + hotpot_delete_and_notify('hotpot_responses', $select, get_string('answer', 'quiz')); } } @@ -263,7 +326,13 @@ function hotpot_print_report_heading(&$course, &$cm, &$hotpot, &$mode) { $navigation = "id>$strmodulenameplural -> "; $navigation .= "id\">$hotpot->name -> "; if (isteacher($course->id)) { - $navigation .= get_string("report$mode", "quiz"); + if ($mode=='overview' || $mode=='simplestat' || $mode=='fullstat') { + $module = "quiz"; + } else { + $module = "hotpot"; + } + $navigation .= get_string("report$mode", $module); + } else { $navigation .= get_string("report", "quiz"); } @@ -276,19 +345,33 @@ function hotpot_print_report_heading(&$course, &$cm, &$hotpot, &$mode) { print_heading($hotpot->name); } -function hotpot_print_report_selector(&$course, &$hotpot, &$mode, &$reportcourse, &$reportusers, &$reportattempts) { +function hotpot_print_report_selector(&$course, &$hotpot, &$formdata) { global $CFG; - $reports = hotpot_get_report_names('overview,simplestat'); + $reports = hotpot_get_report_names('overview,simplestat,fullstat'); print '
wwwroot/mod/hotpot/report.php?hp=$hotpot->id".'">'; - print ''; + print '
'; $menus = array(); + + $menus['mode'] = array(); + foreach ($reports as $name) { + if ($name=='overview' || $name=='simplestat' || $name=='fullstat') { + $module = "quiz"; // standard reports + } else if ($name=='click' && empty($hotpot->clickreporting)) { + $module = ""; // clickreporting is disabled + } else { + $module = "hotpot"; // custom reports + } + if ($module) { + $menus['mode'][$name] = get_string("report$name", $module); + } + } if (isadmin()) { $menus['reportcourse'] = array( - 'this' => get_string('thiscourse', 'hotpot'), + 'this' => get_string('thiscourse', 'hotpot'), // $course->shortname, 'all' => get_string('allmycourses', 'hotpot') ); } @@ -297,23 +380,54 @@ function hotpot_print_report_selector(&$course, &$hotpot, &$mode, &$reportcourse 'all' => get_string('allparticipants') ); $menus['reportattempts'] = array( - 'best' => get_string('bestattempt', 'hotpot'), - 'all' => get_string('allattempts', 'hotpot') + 'all' => get_string('attemptsall', 'hotpot'), + 'best' => get_string('attemptsbest', 'hotpot'), + 'first' => get_string('attemptsfirst', 'hotpot'), + 'last' => get_string('attemptslast', 'hotpot') ); - print ''; + }; + print ''; - print ''; - foreach ($reports as $name) { - print ''; + $menus = array(); + $menus['reportformat'] = array( + 'htm' => get_string('reportformathtml', 'hotpot'), + 'xls' => get_string('reportformatexcel', 'hotpot'), + 'txt' => get_string('reportformattext', 'hotpot'), + ); + if (trim($CFG->hotpot_excelencodings)) { + $menus['reportencoding'] = array(get_string('none')=>''); + + $encodings = explode(',', $CFG->hotpot_excelencodings); + foreach ($encodings as $encoding) { + + $encoding = trim($encoding); + if ($encoding) { + $menus['reportencoding'][$encoding] = $encoding; + } + } + } + $menus['reportwrapdata'] = array( + '1' => get_string('yes'), + '0' => get_string('no'), + ); + $menus['reportshowlegend'] = array( + '1' => get_string('yes'), + '0' => get_string('no'), + ); + + print ''; + foreach ($menus as $name => $options) { + $value = $formdata[$name]; + print ''; } print ''; @@ -332,7 +446,7 @@ function hotpot_get_report_names($names='') { $names = explode(',', $names); } - $plugins = get_list_of_plugins("mod/hotpot/report"); + $plugins = get_list_of_plugins('mod/hotpot/report'); foreach($names as $name) { if (is_numeric($i = array_search($name, $plugins))) { $reports[] = $name; @@ -345,18 +459,18 @@ function hotpot_get_report_names($names='') { return $reports; } -function hotpot_get_report_users($course_ids, $reportusers) { +function hotpot_get_report_users($course_ids, $formdata) { $users = array(); /// Check to see if groups are being used in this module $currentgroup = false; if ($groupmode = groupmode($course, $cm)) { // Groups are being used - $currentgroup = setup_and_print_groups($course, $groupmode, "report.php?id=$cm->id&mode=simplestat"); + $currentgroup = setup_and_print_groups($course, $groupmode, "report.php?id=$cm->id&mode=simple"); } $sort = "u.lastname ASC"; - switch ($reportusers) { + switch ($formdata['reportusers']) { case 'students': if ($currentgroup) { $users = get_group_students($currentgroup, $sort); @@ -375,4 +489,46 @@ function hotpot_get_report_users($course_ids, $reportusers) { return $users; } +function hotpot_get_records_groupby($function, $fieldnames, $table, $select, $groupby) { + + global $CFG; + + switch (strtolower($CFG->dbtype)) { + case 'mysql': + $fields = "$groupby, $function(CONCAT(".join(",'_',", $fieldnames).")) AS joinedvalues"; + break; + case 'postgres7': + $fields = "$groupby, $function(".join("||'_'||", $fieldnames).") AS joinedvalues"; + break; + default: + $fields = ""; + break; + } + + if ($fields) { + $records = get_records_sql("SELECT $fields FROM $table WHERE $select GROUP BY $groupby"); + } + + if (empty($fields) || empty($records)) { + $records = array(); + } + + $fieldcount = count($fieldnames); + + foreach ($records as $id=>$record) { + if (empty($record->joinedvalues)) { + unset($records[$id]); + } else { + $formdata = explode('_', $record->joinedvalues); + + for ($i=0; $i<$fieldcount; $i++) { + $fieldname = $fieldnames[$i]; + $records[$id]->$fieldname = $formdata[$i]; + } + } + unset($record->joinedvalues); // tidy up + } + + return $records; +} ?> diff --git a/mod/hotpot/report/default.php b/mod/hotpot/report/default.php index 31412d0e5c7..97dd0f138e2 100644 --- a/mod/hotpot/report/default.php +++ b/mod/hotpot/report/default.php @@ -17,13 +17,14 @@ class hotpot_default_report { - function display($cm, $course, $hotpot) { /// This function just displays the report + function display($hotpot, $cm, $course, $users, $attempts, $questions, $options) { + /// This function just displays the report + // it is replaced by the "display" functions in the scripts in the "report" folder return true; } function add_question_headings(&$questions, &$table, $align='center', $size=50, $wrap=false, $fontsize=0) { $count = count($questions); - $questionids = array_keys($questions); for ($i=0; $i<$count; $i++) { $table->head[] = get_string('questionshort', 'hotpot', $i+1); if (isset($table->align)) { @@ -42,6 +43,81 @@ class hotpot_default_report { } + function set_legend(&$table, &$q, &$value, &$question) { + + // check the legend is required + if (isset($table->legend) && isset($value)) { + + // create question details array, if necessary + if (empty($table->legend[$q])) { + $table->legend[$q] = array( + 'name' => hotpot_get_question_name($question), + 'answers' => array() + ); + } + + // search for this $value in answers array for this $q(uestion) + $i_max = count($table->legend[$q]['answers']); + for ($i=0; $i<$i_max; $i++) { + if ($table->legend[$q]['answers'][$i]==$value) { + break; + } + } + + // add $value to answers array, if it was not there + if ($i==$i_max) { + $table->legend[$q]['answers'][$i] = $value; + } + + // convert $value to alphabetic index (A, B ... AA, AB ...) + $value = $this->dec_to_ALPHA($i); + } + } + function create_legend_table(&$tables, &$table) { + + if (isset($table->legend)) { + + $legend->width = '*'; + $legend->tablealign = '*'; + $legend->border = isset($table->border) ? $table->border : NULL; + $legend->cellpadding = isset($table->cellpadding) ? $table->cellpadding : NULL; + $legend->cellspacing = isset($table->cellspacing) ? $table->cellspacing : NULL; + $legend->tableclass = isset($table->tableclass) ? $table->tableclass : NULL; + + $legend->caption = get_string('reportlegend', 'hotpot'); + $legend->align = array('right', 'left'); + $legend->statheadercols = array(0); + + $legend->stat = array(); + + // put the questions in order + sort($table->legend); + + foreach($table->legend as $q=>$question) { + + $legend->stat[] = array( + get_string('questionshort', 'hotpot', $q+1), + $question['name'] + ); + foreach($question['answers'] as $a=>$answer) { + $legend->stat[] = array( + $this->dec_to_ALPHA($a), + $answer + ); + } + } + + unset($table->legend); + $tables[] = $legend; + } + } + function dec_to_ALPHA($dec) { + if ($dec < 26) { + return chr(ord('A') + $dec); + } else { + return $this->dec_to_ALPHA(intval($dec/26)-1).$this->dec_to_ALPHA($dec % 26); + } + } function remove_column(&$col, &$table) { if (is_array($table)) { @@ -106,6 +182,12 @@ class hotpot_default_report { // $table->head is an array of headings (all TH cells) // $table->data[] is an array of arrays containing the data (all TD cells) // if a row is given as "hr", a "tabledivider" is inserted +// if a cell is a string, it is assumed to be the cell content +// a cell can also be an object, thus: +// $cell->text : the content of the cell +// $cell->rowspan : the row span of this cell +// $cell->colspan : the column span of this cell +// if rowspan or colspan are specified, neighboring cells are shifted accordingly // $table->stat[] is an array of arrays containing the statistics rows (TD and TH cells) // $table->foot[] is an array of arrays containing the footer rows (all TH cells) @@ -113,45 +195,88 @@ class hotpot_default_report { ////////////////////////////////////////// -/// print an html report +/// print a report - function print_html(&$cm, &$table, $mode, $tablename='') { - - $this->print_html_table($table); - - // print buttons, if required - if (isset($mode) && !empty($table)) { - - // button options - $options = array( - 'id'=>"$cm->id", - 'mode'=>$mode, - 'noheader'=>'yes' - ); - if ($tablename) { - $options['tablename'] = $tablename; - } - - print '
'; + print '
'; + helpbutton('reportcontent', get_string('reportcontent', 'hotpot'), 'hotpot'); + print ''.get_string('reportcontent', 'hotpot').':'; foreach ($menus as $name => $options) { - eval('$value=$'.$name.';'); + $value = $formdata[$name]; print choose_from_menu($options, $name, $value, "", "", 0, true); - } - if (isteacher($course->id)) { - helpbutton('reportselector', get_string('report'), 'hotpot'); - } - print '
'; + helpbutton('reportformat', get_string('reportformat', 'hotpot'), 'hotpot'); + print ''.get_string($name, 'hotpot').':'.choose_from_menu($options, $name, $value, "", "", 0, true).'
'; - print '
'; - - $options["download"] = "xls"; - print_single_button("report.php", $options, get_string("downloadexcel")); - - print ''; - - $options["download"] = "txt"; - print_single_button("report.php", $options, get_string("downloadtext")); - - print '
'; + function print_report(&$course, &$hotpot, &$tables, &$options) { + switch ($options['reportformat']) { + case 'txt': + $this->print_text_report($course, $hotpot, $tables, $options); + break; + case 'xls': + $this->print_excel_report($course, $hotpot, $tables, $options); + break; + default: // 'htm' (and anything else) + $this->print_html_report($tables); + break; } } - function print_html_table($table) { - global $THEME; - // do nothing if the table is empty - if (empty($table) || $this->set_colspan($table)==0) return true; - + function print_report_start(&$course, &$hotpot, &$options, &$table) { + switch ($options['reportformat']) { + case 'txt': + $this->print_text_start($course, $hotpot, $options); + break; + case 'xls': + $this->print_excel_start($course, $hotpot, $options); + break; + + case 'htm': + $this->print_html_start($course, $hotpot, $options); + break; + } + } + + function print_report_cells(&$table, &$options, $zone) { + switch ($options['reportformat']) { + case 'txt': + $fmt = 'text'; + break; + case 'xls': + $fmt = 'excel'; + break; + default: // 'htm' (and anything else) + $fmt = 'html'; + break; + } + $fn = "print_{$fmt}_{$zone}"; + $this->$fn($table, $options); + } + + function print_report_finish(&$course, &$hotpot, &$options) { + switch ($options['reportformat']) { + case 'txt' : + // do nothing + break; + case 'xls': + $this->print_excel_finish($course, $hotpot, $options); + break; + case 'htm': + $this->print_html_finish($course, $hotpot, $options); + break; + } + } + +////////////////////////////////////////// +/// print an html report + + function print_html_report(&$tables) { + $count = count($tables); + foreach($tables as $i=>$table) { + + $this->print_html_start($table); + $this->print_html_head($table); + $this->print_html_data($table); + $this->print_html_stat($table); + $this->print_html_foot($table); + $this->print_html_finish($table); + + if (($i+1)<$count) { + print_spacer(30, 10, true); + } + } + } + function print_html_start(&$table) { + // default class for the table if (empty($table->tableclass)) { $table->tableclass = 'generaltable'; @@ -161,12 +286,24 @@ class hotpot_default_report { $d = $table->tableclass.'cell'; $h = $table->tableclass.'header'; - $th_side = NULL; - $th_side->start = ''; - $th_side->end = ''."\n"; + $table->th_side = ''; - $td = array(); - $th_top = array(); + $table->td = array(); + $table->th_top = array(); + + if (empty($table->colspan)) { + if (isset($table->head)) { + $table->colspan = count($table->head); + } else if (isset($table->data)) { + $table->colspan = count($table->data[0]); + } else if (isset($table->stat)) { + $table->colspan = count($table->stat); + } else if (isset($table->foot)) { + $table->colspan = count($table->foot); + } else { + $table->colspan = 0; + } + } for ($i=0; $i<$table->colspan; $i++) { @@ -175,15 +312,12 @@ class hotpot_default_report { $size = empty($table->size[$i]) ? '' : ' width="'.$table->size[$i].'"'; $wrap = empty($table->wrap[$i]) ? '' : ' nowrap="nowrap"'; - $th_top[$i]->start = ''; - $th_top[$i]->end = ''."\n"; + $table->th_top[$i] = ''; - $td[$i]->start = ''; - $td[$i]->end = ''."\n"; + $table->td[$i] = ''; if (!empty($table->fontsize[$i])) { - $td[$i]->start .= ''; - $td[$i]->end = ''.$td[$i]->end; + $table->td[$i] .= ''; } } @@ -199,135 +333,184 @@ class hotpot_default_report { if (empty($table->width)) { $table->width = "80%"; // actually the width of the "simple box" } + if (empty($table->tablealign)) { + $table->tablealign = "center"; + } - print_simple_box_start("center", "$table->width", "#ffffff", 0); + if (isset($table->start)) { + print $table->start."\n"; + } + + print_simple_box_start("$table->tablealign", "$table->width", "#ffffff", 0); print ''."\n"; if (isset($table->caption)) { print ''."\n"; } + } + function print_html_head(&$table) { if (isset($table->head)) { - print ''."\n"; - foreach ($table->head as $i => $cell) { - print $th_top[$i]->start.$cell.$th_top[$i]->end; + print "\n"; + foreach ($table->head as $i=>$cell) { + $th = $table->th_top[$i]; + print $th.$cell."\n"; } - print ''."\n"; + print "\n"; } - + } + function print_html_data(&$table) { if (isset($table->data)) { + $skipcell = array(); foreach ($table->data as $cells) { - print ''."\n"; - if (is_array($cells)) { - foreach ($cells as $i => $cell) { - print $td[$i]->start.$cell.$td[$i]->end; - } - } else if ($cells == 'hr') { + print "\n"; + if (is_array($cells)) { + $i = 0; // index on $cells + $col = 0; // column index + while ($col<$table->colspan && isset($cells[$i])) { + if (empty($skipcell[$col])) { + $cell = &$cells[$i++]; + $td = $table->td[$col]; + if (is_object($cell)) { + $text = $cell->text; + if (isset($cell->rowspan) && is_numeric($cell->rowspan) && ($cell->rowspan>0)) { + $td = '\n"; + } else { + $skipcell[$col]--; + } + $col++; + } // end while + } else if ($cells=='hr') { print ''."\n"; } - print ''."\n"; + print "\n"; } } - + } + function print_html_stat(&$table) { if (isset($table->stat)) { if (empty($table->statheadercols)) { $table->statheadercols = array(); } foreach ($table->stat as $cells) { - print ''."\n"; + print ''; foreach ($cells as $i => $cell) { if (in_array($i, $table->statheadercols)) { - print $th_side->start.$cell.$th_side->end; + $th = $table->th_side; + print $th.$cell."\n"; } else { - print $td[$i]->start.$cell.$td[$i]->end; + $td = $table->td[$i]; + print $td.$cell."\n"; } } - print ''."\n"; + print "\n"; } } - + } + function print_html_foot(&$table) { if (isset($table->foot)) { foreach ($table->foot as $cells) { - print ''."\n"; + print "\n"; foreach ($cells as $i => $cell) { if ($i==0) { - print $th_side->start.$cell.$th_side->end; + $th = $table->th_side; + print $th.$cell."\n"; } else { - print $th_top[$i]->start.$cell.$th_top[$i]->end; + $th = $table->th_top[$i]; + print $th.$cell."\n"; } } - print ''."\n"; + print "\n"; } } - print '
'.$table->caption.'
rowspan-1; + } + if (isset($cell->colspan) && is_numeric($cell->colspan) && ($cell->colspan>0)) { + $td = 'colspan; $c++) { + if (empty($skipcell[$col+$c])) { + $skipcell[$col+$c] = 1; + } else { + $skipcell[$col+$c] ++; + } + } + } + } else { // $cell is a string + $text = $cell; + } + print $td.$text."
'."\n"; + } + function print_html_finish(&$table) { + print "\n"; print_simple_box_end(); - return true; - } - function set_colspan(&$table) { - if (empty($table->colspan)) { - if (isset($table->head)) { - $table->colspan = count($table->head); - } else if (isset($table->data)) { - $table->colspan = count($table->data[0]); - } else if (isset($table->stat)) { - $table->colspan = count($table->stat); - } else if (isset($table->foot)) { - $table->colspan = count($table->foot); - } else { - $table->colspan = 0; - } + if (isset($table->finish)) { + print $table->finish."\n"; } - return $table->colspan; } ////////////////////////////////////////// /// print a text report - function print_text(&$course, &$hotpot, &$table) { - $this->print_text_headers($course, $hotpot); - $this->print_text_head($table); - $this->print_text_data($table); - $this->print_text_stat($table); - $this->print_text_foot($table); + function print_text_report(&$course, &$hotpot, &$tables, &$options) { + $this->print_text_start($course, $hotpot, $options); + foreach ($tables as $table) { + $this->print_text_head($table, $options); + $this->print_text_data($table, $options); + $this->print_text_stat($table, $options); + $this->print_text_foot($table, $options); + } } - function print_text_headers($course, $hotpot) { + function print_text_start(&$course, &$hotpot, &$options) { header("Content-Type: application/download\n"); - header("Content-Disposition: attachment; filename=$course->shortname $hotpot->name.txt"); + header("Content-Disposition: attachment; filename=$course->shortname-$hotpot->name.txt"); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0,pre-check=0"); header("Pragma: public"); } - function print_text_head(&$table) { + function print_text_head(&$table, &$options) { + if (isset($table->caption)) { + $i = strlen($table->caption); + $data = array( + array(str_repeat('=', $i)), + array($table->caption), + array(str_repeat('=', $i)), + ); + foreach($data as $cells) { + $this->print_text_cells($cells, $options); + } + } if (isset($table->head)) { - $this->print_text_cells($table->head); + $this->print_text_cells($table->head, $options); } } - function print_text_data(&$table) { + function print_text_data(&$table, &$options) { if (isset($table->data)) { foreach ($table->data as $cells) { - $this->print_text_cells($cells); + $this->print_text_cells($cells, $options); } } } - function print_text_stat(&$table) { + function print_text_stat(&$table, &$options) { if (isset($table->stat)) { foreach ($table->stat as $cells) { - $this->print_text_cells($cells); + $this->print_text_cells($cells, $options); } } } - function print_text_foot(&$table) { + function print_text_foot(&$table, &$options) { if (isset($table->foot)) { foreach ($table->foot as $cells) { - $this->print_text_cells($cells); + $this->print_text_cells($cells, $options); } } } - function print_text_cells(&$cells) { + function print_text_cells(&$cells, &$options) { // do nothing if there are no cells - if (empty($cells)) return; + if (empty($cells) || is_string($cells)) return; // convert to tab-delimted string $str = implode("\t", $cells); @@ -349,55 +532,65 @@ class hotpot_default_report { ////////////////////////////////////////// /// print an Excel report - function print_excel(&$course, &$hotpot, &$table) { + function print_excel_report(&$course, &$hotpot, &$tables, &$options) { global $CFG; require_once("$CFG->libdir/excel/Worksheet.php"); require_once("$CFG->libdir/excel/Workbook.php"); - // array of format objects (one for each cell) - $fmt = array(); + // send headers to browser + $this->print_excel_start($course, $hotpot, $options); - // Create a new workbook and worksheet - $wb = new Workbook("-"); - $ws = &$wb->add_worksheet(get_string('reportsimplestat', 'quiz')); - - $this->print_excel_headers($course, $hotpot); - $this->print_excel_head($wb, $ws, $table, $fmt); - $this->print_excel_data($wb, $ws, $table, $fmt); - $this->print_excel_stat($wb, $ws, $table, $fmt); - $this->print_excel_foot($wb, $ws, $table, $fmt); + // Create a new workbook + $wb = new Workbook("-"); // $course->shortname + foreach($tables as $table) { + // create a worksheet for this table + unset($ws); + $ws = &$wb->add_worksheet(empty($table->caption) ? '' : strip_tags($table->caption)); + + $row = 0; + $this->print_excel_head($wb, $ws, $table, $row, $options); + $this->print_excel_data($wb, $ws, $table, $row, $options); + $this->print_excel_stat($wb, $ws, $table, $row, $options); + $this->print_excel_foot($wb, $ws, $table, $row, $options); + } + + // close the workbook (and send it to the browser) $wb->close(); } - function print_excel_headers(&$course, &$instance) { + function print_excel_start(&$course, &$hotpot, &$options) { header("Content-type: application/vnd.ms-excel"); - header("Content-Disposition: attachment; filename=$course->shortname $instance->name.xls" ); + header("Content-Disposition: attachment; filename=$course->shortname $hotpot->name.xls" ); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0,pre-check=0"); header("Pragma: public"); } - function print_excel_head(&$wb, &$ws, &$table, &$fmt) { + function print_excel_head(&$wb, &$ws, &$table, &$row, &$options) { // format properties - $properties = array('bold'=>1, 'align'=>'center'); - + $properties = array( + 'bold'=>1, + 'align'=>'center', + 'valign'=>'bottom', + 'text_wrap'=>1 + ); // print the headings - $this->print_excel_cells($wb, $ws, $table, $fmt, $properties, $table->head); - } - function print_excel_data(&$wb, &$ws, &$table, &$fmt) { + $this->print_excel_cells($wb, $ws, $table, $row, $properties, $table->head, $options); + } + function print_excel_data(&$wb, &$ws, &$table, &$row, &$options) { // do nothing if there are no cells if (empty($table->data)) return; // format properties - $properties = array(); + $properties = array('text_wrap' => (empty($options['reportwrapdata']) ? 0 : 1)); // print the data cells foreach ($table->data as $cells) { - $this->print_excel_cells($wb, $ws, $table, $fmt, $properties, $cells, array()); + $this->print_excel_cells($wb, $ws, $table, $row, $properties, $cells, $options); } - } - function print_excel_stat(&$wb, &$ws, &$table, &$fmt) { + } + function print_excel_stat(&$wb, &$ws, &$table, &$row, &$options) { // do nothing if there are no cells if (empty($table->stat)) return; @@ -413,10 +606,10 @@ class hotpot_default_report { $properties['bottom'] = ($i==($i_count-1)) ? 1 : 0; // print this row of statistics - $this->print_excel_cells($wb, $ws, $table, $fmt, $properties, $cells, $table->statheadercols); + $this->print_excel_cells($wb, $ws, $table, $row, $properties, $cells, $options, $table->statheadercols); } - } - function print_excel_foot(&$wb, &$ws, &$table, &$fmt) { + } + function print_excel_foot(&$wb, &$ws, &$table, &$row, &$options) { // do nothing if there are no cells if (empty($table->foot)) return; @@ -432,54 +625,87 @@ class hotpot_default_report { $properties['bottom'] = ($i==($i_count-1)) ? 1 : 0; // print this footer row - $this->print_excel_cells($wb, $ws, $table, $fmt, $properties, $cells); + $this->print_excel_cells($wb, $ws, $table, $row, $properties, $cells, $options); } - } - function print_excel_cells(&$wb, &$ws, &$table, &$fmt, &$properties, &$cells, $statheadercols=NULL) { + } + + function print_excel_cells(&$wb, &$ws, &$table, &$row, &$properties, &$cells, &$options, $statheadercols=NULL) { // do nothing if there are no cells - if (empty($cells)) return; - - // next row - $row = count($fmt); - - // initialize formats for this row - $fmt[$row] = array(); + if (empty($cells) || is_string($cells)) return; foreach($cells as $col => $cell) { - if ($row==0) { - // set column width (excel-width = web-width / 5) - $width = (isset($table->size[$col]) && is_numeric($table->size[$col])) ? ceil($table->size[$col]/5) : ($col==0 ? 30 : 15); - //$ws->set_column($col, $col, $width); - } - - // set text wrap if $cell is a string and contains a newline - if (is_string($cell) && preg_match("/\n/", $cell)) { - $properties['text_wrap'] = 1; + unset($fmt_properties); + $fmt_properties = $properties; + + if (empty($fmt_properties['text_wrap'])) { + if (strlen("$cell")>=9) { + // long cell value + $fmt_properties['align'] = 'left'; + } } else { - $properties['text_wrap'] = 0; + if (strlen("$cell")<9 && strpos("$cell", "\n")===false) { + // short cell value (wrapping not required) + $fmt_properties['text_wrap'] = 0; + } } // set bold, if required (for stat) if (isset($statheadercols)) { - $properties['bold'] = in_array($col, $statheadercols) ? 1 : 0; - $properties['align'] = in_array($col, $statheadercols) ? 'right' : $table->align[$col]; + $fmt_properties['bold'] = in_array($col, $statheadercols) ? 1 : 0; + $fmt_properties['align'] = in_array($col, $statheadercols) ? 'right' : $table->align[$col]; } - // create (a reference to) a new format object - $fmt[$row][$col] = &$wb->add_format($properties); - - // set vertical alignment - $fmt[$row][$col]->set_align('top'); + // set align, if required + if (isset($table->align[$col]) && empty($fmt_properties['align'])) { + $fmt_properties['align'] = $table->align[$col]; + } + + // check to see that an identical format object has not already been created + unset($fmt); + foreach ($wb->formats as $id=>$format) { + if (isset($format->properties) && $format->properties==$fmt_properties) { + $fmt = &$wb->formats[$id]; + break; + } + } + + // create new format object, if necessary (to avoid "too many cell formats" error) + if (!isset($fmt)) { + $fmt = &$wb->add_format($fmt_properties); + $fmt->properties = &$fmt_properties; + + // set vertical alignment + if (isset($fmt->properties['valign'])) { + $fmt->set_align($fmt->properties['valign']); + } else { + $fmt->set_align('top'); // default + } + } // write cell - if (is_numeric($cell)) { - $ws->write_number($row, $col, $cell, $fmt[$row][$col]); + if (is_numeric($cell) && !preg_match("/^0./", $cell)) { + $ws->write_number($row, $col, $cell, $fmt); } else { - $ws->write_string($row, $col, $cell, $fmt[$row][$col]); + if (!empty($options['reportencoding'])) { + $in_charset = ''; + if (function_exists('mb_convert_encoding')) { + $in_charset = mb_detect_encoding($cell, 'auto'); + } + if (empty($in_charset)) { + $in_charset = get_string('thischarset'); + } + if ($in_charset != 'ASCII' && function_exists('mb_convert_encoding')) { + $cell = mb_convert_encoding($cell, $options['reportencoding'], $in_charset); + } + } + $ws->write_string($row, $col, $cell, $fmt); } - } + } // end foreach $col + + // increment $row + $row++; } } diff --git a/mod/hotpot/report/fullstat/report.php b/mod/hotpot/report/fullstat/report.php index 260a680d963..ef897788663 100644 --- a/mod/hotpot/report/fullstat/report.php +++ b/mod/hotpot/report/fullstat/report.php @@ -4,108 +4,102 @@ class hotpot_report extends hotpot_default_report { - function display(&$hotpot, &$cm, &$course, &$users, &$attempts, &$questions) { - + function display(&$hotpot, &$cm, &$course, &$users, &$attempts, &$questions, &$options) { global $CFG; - // retrieve form variables, if any - global $download, $tablename; - optional_variable($download, ""); - optional_variable($tablename, ""); - - $strbestgrade = "highest"; // $QUIZ_GRADE_METHOD[$hotpot->grademethod]; - - // get responses for the attempts by these users - foreach ($attempts as $a => $attempt) { - - // initialize the responses array for this attempt + foreach ($attempts as $a=>$attempt) { $attempts[$a]->responses = array(); + } + foreach ($questions as $q=>$question) { + $questions[$q]->attempts = array(); + } - foreach ($questions as $q=>$question) { + // get reponses to these attempts + $attempt_ids = join(',',array_keys($attempts)); + if (!$responses = get_records_sql("SELECT * FROM {$CFG->prefix}hotpot_responses WHERE attempt IN ($attempt_ids)")) { + $responses = array(); + } - if (!isset($questions[$q]->attempts)) { - $questions[$q]->attempts = array(); - } + // ids of questions used in these responses + $questionids = array(); - // get the response, if any, to this question on this attempt - if ($response = get_record('hotpot_responses', 'attempt', $attempt->id, 'question', $question->id)) { + foreach ($responses as $response) { + // shortcuts to the attempt and question ids + $a = $response->attempt; + $q = $response->question; - // add the response for this attempt - $attempts[$a]->responses[$q] = $response; + // check the attempt and question objects exist + // (if they don't exist, something is very wrong!) + if (isset($attempts[$a]) || isset($questions[$q])) { + + // add the response for this attempt + $attempts[$a]->responses[$q] = $response; + + // add a reference from the question to the attempt which includes this question + $questions[$q]->attempts[] = &$attempts[$a]; - // add a reference from the question to the attempt which includes this question - $questions[$q]->attempts[] = &$attempts[$a]; - } + // flag this id as being used + $questionids[$q] = true; + } + } + + // remove unused questions + $questionids = array_keys($questionids); + foreach ($questions as $id=>$question) { + if (!in_array($id, $questionids)) { + unset($questions[$id]); } } // create the tables - $this->create_responses_table($users, $attempts, $questions, $r_table=NULL, $download, $course, $hotpot); - $this->create_analysis_table($users, $attempts, $questions, $a_table=NULL, $download); + $tables = array(); + $this->create_responses_table($hotpot, $course, $users, $attempts, $questions, $options, $tables); + $this->create_analysis_table($users, $attempts, $questions, $options, $tables); - switch ($download) { - case 'txt': - switch ($tablename) { - case 'r': - $this->print_text($course, $hotpot, $r_table); - break; - case 'a': - $this->print_text($course, $hotpot, $a_table); - break; - } - break; - - case 'xls': - switch ($tablename) { - case 'r': - $this->print_excel($course, $hotpot, $r_table); - break; - case 'a': - $this->print_excel($course, $hotpot, $a_table); - break; - } - break; - - default: - $this->print_html($cm, $r_table, 'fullstat', 'r'); - print_spacer(50, 10, true); - - $this->print_html($cm, $a_table, 'fullstat', 'a'); - } + // print report + $this->print_report($course, $hotpot, $tables, $options); return true; } - function create_responses_table(&$users, &$attempts, &$questions, &$table, $download, &$course, &$hotpot) { + function create_responses_table(&$hotpot, &$course, &$users, &$attempts, &$questions, &$options, &$tables) { global $CFG; + $is_html = ($options['reportformat']=='htm'); + // shortcuts for font tags - $br = $download ? "\n" : "
\n"; - $blank = $download ? "" : ' '; - $font_end = $download ? '' : '
'; - $font_red = $download ? '' : ''; - $font_blue = $download ? '' : ''; - $font_brown = $download ? '' : ''; - $font_green = $download ? '' : ''; - $font_small = $download ? '' : ''; - $nobr_start = $download ? '' : ''; - $nobr_end = $download ? '' : ''; + $br = $is_html ? "
\n" : "\n"; + $blank = $is_html ? ' ' : ""; + $font_end = $is_html ? '
' : ''; + $font_red = $is_html ? '' : ''; + $font_blue = $is_html ? '' : ''; + $font_brown = $is_html ? '' : ''; + $font_green = $is_html ? '' : ''; + $font_small = $is_html ? '' : ''; + $nobr_start = $is_html ? '' : ''; + $nobr_end = $is_html ? '' : ''; // is review allowed? (do this once here, to save time later) - $allow_review = (!$download && (isteacher($course->id) || $hotpot->review)); + $allow_review = ($is_html && (isteacher($course->id) || $hotpot->review)); // assume penalties column is NOT required $show_penalties = false; // initialize $table + unset($table); $table->border = 1; $table->width = '100%'; + // initialize legend, if necessary + if (!empty($options['reportshowlegend'])) { + $table->legend = array(); + } + // headings for name, attempt number, score/grade and penalties $table->head = array( get_string("name"), - hotpot_grade_heading($hotpot, $download), + hotpot_grade_heading($hotpot, $options), get_string('attempt', 'quiz'), ); $table->align = array('left', 'center', 'center'); @@ -143,7 +137,7 @@ class hotpot_report extends hotpot_default_report { } else { $name = "$u->firstname $u->lastname"; } - if (!$download) { // html + if ($is_html) { $name = ''.$name.''; } $grade = isset($user->grade) ? $user->grade : $blank; @@ -176,58 +170,86 @@ class hotpot_report extends hotpot_default_report { // get responses to questions in this attempt foreach ($attempt->responses as $response) { - // correct - if (!$correct = hotpot_strings($response->correct)) { - $correct = "($strnoresponse)"; - } - $cell = $font_red.$correct.$font_end; + // check this question id is OK (should be) + $col = array_search($response->question, $questionids); + if (is_numeric($col)) { - // wrong - if ($wrong = hotpot_strings($response->wrong)) { - $cell .= $br.$font_blue.$wrong.$font_end; - } - - // ignored - if ($ignored = hotpot_strings($response->ignored)) { - $cell .= $br.$font_brown.$ignored.$font_end; - } - - // numeric - if (is_numeric($response->score)) { - if (empty($table->caption)) { - $table->caption = get_string('indivresp', 'quiz'); - if (!$download) { - $table->caption .= helpbutton('responsestable', $table->caption, 'hotpot', true, false, '', true); - } + // correct + if ($value = hotpot_strings($response->correct)) { + $this->set_legend($table, $col, $value, $questions[$response->question]); + } else { + $value = "($strnoresponse)"; + } + $cell = $font_red.$value.$font_end; + + // wrong + if ($value = hotpot_strings($response->wrong)) { + if (isset($table->legend)) { + $values = array(); + foreach (explode(',', $value) as $v) { + $this->set_legend($table, $col, $v, $questions[$response->question]); + $values[] = $v; + } + $value = implode(',', $values); + } + $cell .= $br.$font_blue.$value.$font_end; + } + + // ignored + if ($value = hotpot_strings($response->ignored)) { + if (isset($table->legend)) { + $values = array(); + foreach (explode(',', $value) as $v) { + $this->set_legend($table, $col, $v, $questions[$response->question]); + $values[] = $v; + } + $value = implode(',', $values); + } + $cell .= $br.$font_brown.$value.$font_end; + } + + // numeric + if (is_numeric($response->score)) { + if (empty($table->caption)) { + $table->caption = get_string('indivresp', 'quiz'); + if ($is_html) { + $table->caption .= helpbutton('responsestable', $table->caption, 'hotpot', true, false, '', true); + } + } + $hints = empty($response->hints) ? 0 : $response->hints; + $clues = empty($response->clues) ? 0 : $response->clues; + $checks = empty($response->checks) ? 0 : $response->checks; + $numeric = $response->score.'% '.$blank.' ('.$hints.','.$clues.','.$checks.')'; + $cell .= $br.$nobr_start.$font_green.$numeric.$font_end.$nobr_end; } - $hints = empty($response->hints) ? 0 : $response->hints; - $clues = empty($response->clues) ? 0 : $response->clues; - $checks = empty($response->checks) ? 0 : $response->checks; - $numeric = $response->score.'% '.$blank.' ('.$hints.','.$clues.','.$checks.')'; - $cell .= $br.$nobr_start.$font_green.$numeric.$font_end.$nobr_end; - } - // add responses into $cells - if (is_numeric($col = array_search($response->question, $questionids))) { $cells[$start_col + $col] = $cell; } } $table->data[] = $cells; } - } // end foreach + + // insert 'tabledivider' between users + $table->data[] = 'hr'; + + } // end foreach $users + // remove final 'hr' from data rows + array_pop($table->data); + if (!$show_penalties) { $col = 3 + count($questionids); $this->remove_column($col, $table); } + + $tables[] = &$table; } - function create_analysis_table(&$users, &$attempts, &$questions, &$table, $download) { + function create_analysis_table(&$users, &$attempts, &$questions, &$options, &$tables) { + + $is_html = ($options['reportformat']=='htm'); // the fields we are interested in, in the order we want them - // currently some fields are redundant for some types of quiz - // so the the fields could also be set depending on quiz type - // (see hotpot_set_fields_by_quiz_type function below) $fields = array('correct', 'wrong', 'ignored', 'hints', 'clues', 'checks', 'weighting'); $string_fields = array('correct', 'wrong', 'ignored'); @@ -245,10 +267,10 @@ class hotpot_report extends hotpot_default_report { foreach ($question->attempts as $attempt) { $scores[] = $attempt->score; } - + // sort scores values (in ascending order) asort($scores); - + // get the borderline high and low scores $count = count($scores); switch ($count) { @@ -265,22 +287,23 @@ class hotpot_report extends hotpot_default_report { $hi_score = $scores[round($count*2/3)]; break; } - - // initialize statistics array for this question - $q[$id] = array(); // get statistics for each attempt which includes this question foreach ($question->attempts as $attempt) { - + $is_hi_score = ($attempt->score >= $hi_score); $is_lo_score = ($attempt->score < $lo_score); - + // reference to the response to the current question $response = &$attempt->responses[$id]; // update statistics for fields in this response foreach($fields as $field) { + if (!isset($q[$id])) { + $q[$id] = array(); + } + if (!isset($f[$field])) { $f[$field] = array('count' => 0); } @@ -329,35 +352,45 @@ class hotpot_report extends hotpot_default_report { } // end foreach question // check we have some details - if ($q) { - + if (count($q)) { $showhideid = 'showhide'; // shortcuts for html tags - $bold_start = $download ? "" : ''; - $bold_end = $download ? "" : ''; - $div_start = $download ? "" : '
'; - $div_end = $download ? "" : '
'; + $bold_start = $is_html ? '' : ""; + $bold_end = $is_html ? '' : ""; + $div_start = $is_html ? '
' : ""; + $div_end = $is_html ? '
' : ""; - $font_red = $download ? '' : ''; - $font_blue = $download ? '' : ''; - $font_green = $download ? '' : ''; - $font_brown = $download ? '' : ''; - $font_end = $download ? '' : ''."\n"; + $font_red = $is_html ? '' : ''; + $font_blue = $is_html ? '' : ''; + $font_green = $is_html ? '' : ''; + $font_brown = $is_html ? '' : ''; + $font_end = $is_html ? ''."\n" : ''; - $br = $download ? "\n" : '
'; - $space = $download ? "" : ' '; - $no_value = $download ? "" : '--'; - $help_button = $download ? "" : helpbutton("discrimination", "", "quiz", true, false, "", true); + $br = $is_html ? '
' : "\n"; + $space = $is_html ? ' ' : ""; + $no_value = $is_html ? '--' : ""; + $help_button = $is_html ? helpbutton("discrimination", "", "quiz", true, false, "", true) : ""; // table properties + unset($table); $table->border = 1; $table->width = '100%'; $table->caption = get_string('itemanal', 'quiz'); - if (!$download) { + if ($is_html) { $table->caption .= helpbutton('analysistable', $table->caption, 'hotpot', true, false, '', true); } + // initialize legend, if necessary + if (!empty($options['reportshowlegend'])) { + if (empty($tables) || empty($tables[0]->legend)) { + $table->legend = array(); + } else { + $table->legend = $tables[0]->legend; + unset($tables[0]->legend); + } + } + // headings for name, attempt number and score/grade $table->head = array($space); $table->align = array('right'); @@ -384,20 +417,24 @@ class hotpot_report extends hotpot_default_report { //////////////////////////////////////////// // add $stat(istics) and $foot of $table - $questionids = array_keys($questions); - foreach ($questionids as $col => $id) { + $questionids = array_keys($q); + foreach ($questionids as $col => $id) { $row = 0; - // add button to show/hide question text - if (!isset($table->stat[0])) { - $button = $download ? "" : hotpot_showhide_button($showhideid); - $table->stat[0] = array(get_string('question', 'quiz').$button); - } + // print the question text if there is no legend + if (empty($table->legend)) { - // add the question name/text - $name = hotpot_get_question_name($questions[$id]); - $table->stat[$row++][$col+1] = $div_start.$bold_start.$name.$bold_end.$div_end.$space; + // add button to show/hide question text + if (!isset($table->stat[0])) { + $button = $is_html ? hotpot_showhide_button($showhideid) : ""; + $table->stat[0] = array(get_string('question', 'quiz').$button); + } + + // add the question name/text + $name = hotpot_get_question_name($questions[$id]); + $table->stat[$row++][$col+1] = $div_start.$bold_start.$name.$bold_end.$div_end.$space; + } // add details about each field foreach ($fields as $field) { @@ -407,14 +444,15 @@ class hotpot_report extends hotpot_default_report { $values = array(); $string_type = array_search($field, $string_fields); - + // get the value of each response to this field // and the count of that value foreach ($q[$id][$field] as $value => $count) { - + if (is_numeric($value) && $count) { if (is_numeric($string_type)) { $value = hotpot_string($value); + $this->set_legend($table, $col, $value, $questions[$id]); switch ($string_type) { case 0: // correct $font_start = $font_red; @@ -431,17 +469,17 @@ class hotpot_report extends hotpot_default_report { } $values[] = $font_start.round(100*$count/$q[$id]['count']['total']).'%'.$font_end.' '.$value; } - + } // end foreach $value => $count - + // initialize stat(istics) row for this field, if required if (!isset($table->stat[$row])) { $table->stat[$row] = array(get_string($field, 'hotpot')); } - + // sort the values by frequency (using user-defined function) usort($values, "hotpot_sort_stat_values"); - + // add stat(istics) values for this field $table->stat[$row++][$col+1] = count($values) ? implode($br, $values) : $space; } @@ -477,12 +515,16 @@ class hotpot_report extends hotpot_default_report { } // end foreach $question ($col) // add javascript to show/hide question text - if (isset($table->stat[0]) && !$download) { + if (isset($table->stat[0]) && $is_html && empty($table->legend)) { $i = count($table->stat[0]); $table->stat[0][$i-1] .= hotpot_showhide_set($showhideid); } - } - } + + $tables[] = &$table; + $this->create_legend_table($tables, $table); + + } // end if (empty($q) + } // end function } // end class function hotpot_sort_stat_values($a, $b) { @@ -552,50 +594,4 @@ return <<type) { - case 1: // jcb - break; - case 2: // jcloze - $fields['correct'] = true; - $fields['wrong'] = true; - $fields['ignored'] = true; - $fields['clues'] = true; - break; - case 3: // jcross - $fields['correct'] = true; - break; - case 4: // jmix - $fields['correct'] = true; - $fields['ignored'] = true; - $fields['checks'] = true; - break; - case 5: // jmatch - $fields['correct'] = true; - $fields['checks'] = true; - break; - case 6: // jmatch - case 6.1: // multi-choice - case 6.2: // short-answer - case 6.3: // hybrid - case 6.4: // multi-select - $fields['correct'] = true; - $fields['wrong'] = true; - $fields['ignored'] = true; - $fields['hints'] = true; - $fields['checks'] = true; - break; - } - } - $fields['weighting'] = true; - - $fields = array_keys($fields); - return $fields; -} ?> diff --git a/mod/hotpot/report/overview/report.php b/mod/hotpot/report/overview/report.php index 4cce8411128..aa63b3ead12 100644 --- a/mod/hotpot/report/overview/report.php +++ b/mod/hotpot/report/overview/report.php @@ -4,144 +4,176 @@ class hotpot_report extends hotpot_default_report { - function display(&$hotpot, &$cm, &$course, &$users, &$attempts, &$questions) { - - // retrieve form variables, if any - global $download, $tablename; - optional_variable($download, ""); - optional_variable($tablename, ""); + function display(&$hotpot, &$cm, &$course, &$users, &$attempts, &$questions, &$options) { - // message strings - $strdeletecheck = get_string('deleteattemptcheck','quiz'); + $this->create_overview_table($hotpot, $cm, $course, $users, $attempts, $questions, $options, $tables=array()); - // create scores table - $this->create_overview_table($users, $attempts, $questions, $s_table=NULL, $download, $course, $hotpot, $abandoned=0); - - if (isteacher($course->id)) { - $this->print_javascript(); - $onsub = "return delcheck('".$strdeletecheck."', 'selection')"; - - // print buttons - echo ''."\n"; - echo ''."\n"; - echo ''."\n"; - } - - // print scores table - $this->print_html_table($s_table); - - if (isteacher($course->id)) { - //There might be a more elegant way than using the
tag for this - echo '
'."\n"; - echo ' '."\n"; - if ($abandoned) { - echo ''."\n"; - } - echo ''."\n"; - echo '
'."\n"; - echo ''."\n"; - } + $this->print_report($course, $hotpot, $tables, $options); return true; } - function create_overview_table(&$users, &$attempts, &$questions, &$table, &$download, &$course, &$hotpot, &$abandoned) { + function create_overview_table(&$hotpot, &$cm, &$course, &$users, &$attempts, &$questions, &$options, &$tables) { global $CFG; $strtimeformat = get_string('strftimedatetime'); - $spacer = ' '; - // start the table + $is_html = ($options['reportformat']=='htm'); + + $spacer = $is_html ? ' ' : ' '; + $br = $is_html ? "
\n" : "\n"; + + // initialize $table + unset($table); $table->border = 1; + $table->width = 10; - $table->head = array( - " ", // picture + $table->head = array(); + $table->align = array(); + $table->size = array(); + $table->wrap = array(); + + // picture column, if required + if ($is_html) { + $table->head[] = $spacer; + $table->align[] = 'center'; + $table->size[] = 10; + $table->wrap[] = "nowrap"; + } + + array_push($table->head, get_string("name"), - hotpot_grade_heading($hotpot, $download), + hotpot_grade_heading($hotpot, $options), get_string("attempt", "quiz"), get_string("time", "quiz"), + get_string("reportstatus", "hotpot"), get_string("timetaken", "quiz"), - get_string("score", "quiz"), + get_string("score", "quiz") ); - $table->align = array("center", "left", "center", "center", "left", "left", "center"); - $table->wrap = array("nowrap", "nowrap", "nowrap", "nowrap", "nowrap", "nowrap", "nowrap"); - $table->width = 10; - $table->size = array(10, "*", "*", "*", "*", "*", "*"); + array_push($table->align, "left", "center", "center", "left", "center", "center", "center"); + array_push($table->wrap, "nowrap", "nowrap", "nowrap", "nowrap", "nowrap", "nowrap", "nowrap"); + array_push($table->size, "*", "*", "*", "*", "*", "*", "*"); + + $abandoned = 0; foreach ($users as $user) { // shortcut to user info held in first attempt record $u = &$user->attempts[0]; - $picture = print_user_picture($u->userid, $course->id, $u->picture, false, true); - if (function_exists("fullname")) { - $name = fullname($u); - } else { - $name = "$u->firstname $u->lastname"; + $picture = ''; + $name = fullname($u); + if ($is_html) { + $picture = print_user_picture($u->userid, $course->id, $u->picture, false, true); + $name = ''.$name.''; } - $name = ''.$name.''; $grade = isset($user->grade) ? $user->grade : $spacer; - $attempts = array(); - $starttimes = array(); - $durations = array(); - $scores = array(); + $attemptcount = count($user->attempts); + if ($attemptcount>1) { + $text = $name; + $name = NULL; + $name->text = $text; + $name->rowspan = $attemptcount; + + $text = $grade; + $grade = NULL; + $grade->text = $text; + $grade->rowspan = $attemptcount; + } + + $data = array(); + if ($is_html) { + if ($attemptcount>1) { + $text = $picture; + $picture = NULL; + $picture->text = $text; + $picture->rowspan = $attemptcount; + } + $data[] = $picture; + } + array_push($data, $name, $grade); foreach ($user->attempts as $attempt) { // increment count of abandoned attempts // if attempt is marked as finished but has no score - if (!empty($attempt->timefinish) && !isset($attempt->score)) { + if ($attempt->status==HOTPOT_STATUS_ABANDONED) { $abandoned++; } $attemptnumber = $attempt->attempt; $starttime = trim(userdate($attempt->timestart, $strtimeformat)); - if (isset($attempt->score) && (isteacher($course->id) || $hotpot->review)) { + if ($is_html && isset($attempt->score) && (isteacher($course->id) || $hotpot->review)) { $attemptnumber = ''.$attemptnumber.''; $starttime = ''.$starttime.''; } - if (isteacher($course->id)) { - $checkbox = ''.$spacer; + if ($is_html && isteacher($course->id)) { + $checkbox = ''.$spacer; } else { $checkbox = ''; } - $attempts[] = $attemptnumber; - $starttimes[] = $checkbox.$starttime; - - $durations[] = empty($attempt->timefinish) ? $spacer : format_time($attempt->timefinish - $attempt->timestart); + $timetaken = empty($attempt->timefinish) ? $spacer : format_time($attempt->timefinish - $attempt->timestart); $score = hotpot_format_score($attempt); - if (is_numeric($score) && $score==$user->grade) { // best grade + if ($is_html && is_numeric($score) && $score==$user->grade) { // best grade $score = ''.$score.''; } - $scores[] = $score; + array_push($data, + $attemptnumber, + $checkbox.$starttime, + hotpot_format_status($attempt), + $timetaken, + $score + ); + + $table->data[] = $data; + + $data = array(); } // end foreach $attempt - $attempts = implode("
\n", $attempts); - $starttimes = implode("
\n", $starttimes); - $durations = implode("
\n", $durations); - $scores = implode("
\n", $scores); - - $table->data[] = array ($picture, $name, $grade, $attempts, $starttimes, $durations, $scores); - + $table->data[] = 'hr'; } // end foreach $user + + // remove final 'hr' from data rows + array_pop($table->data); + + // add the "delete" form to the table + if ($options['reportformat']=='htm' && isteacher($course->id)) { + $strdeletecheck = get_string('deleteattemptcheck','quiz'); + + $table->start = $this->deleteform_javascript(); + $table->start .= '
'."\n"; + $table->start .= ''."\n"; + $table->start .= ''."\n"; + + $table->finish = '
'."\n"; + $table->finish .= ' '."\n"; + if ($abandoned) { + $table->finish .= ''."\n"; + } + $table->finish .= ''."\n"; + $table->finish .= '
'."\n"; + $table->finish .= '
'."\n"; + } + + $tables[] = &$table; } - function print_javascript() { + function deleteform_javascript() { $strselectattempt = addslashes(get_string('selectattempt','hotpot')); - print << ', ''); + } + $hp->insert_submission_form($attemptid, '', ''); + + } else { + // HP5 v5 + if ($hotpot->navigation!=HOTPOT_NAVIGATION_BUTTONS) { + $hp->html = preg_replace('#NavBar\+=(.*);#', '', $hp->html); + } + if ($hotpot->navigation==HOTPOT_NAVIGATION_GIVEUP) { + // $hp->insert_giveup_form($attemptid, '', ''); + } + $hp->insert_submission_form($attemptid, "var NavBar='", "';"); } } } + //FEEDBACK = new Array(); + //FEEDBACK[0] = ''; // url of feedback page/script + //FEEDBACK[1] = ''; // array of array('teachername', 'value'); + //FEEDBACK[2] = ''; // 'student name' [formmail only] + //FEEDBACK[3] = ''; // 'student email' [formmail only] + //FEEDBACK[4] = ''; // window width + //FEEDBACK[5] = ''; // window height + //FEEDBACK[6] = ''; // 'Send a message to teacher' [prompt/button text] + //FEEDBACK[7] = ''; // 'Title' + //FEEDBACK[8] = ''; // 'Teacher' + //FEEDBACK[9] = ''; // 'Message' + //FEEDBACK[10] = ''; // 'Close this window' + + $feedback = array(); + switch ($hotpot->studentfeedback) { + + case HOTPOT_FEEDBACK_NONE: + // do nothing + break; + + case HOTPOT_FEEDBACK_WEBPAGE: + if (empty($hotpot->studentfeedbackurl)) { + $hotpot->studentfeedback = HOTPOT_FEEDBACK_NONE; + } else { + $feedback[0] = "'$hotpot->studentfeedbackurl'"; + } + break; + + case HOTPOT_FEEDBACK_FORMMAIL: + $teachers = hotpot_feedback_teachers($course, $hotpot); + if (empty($teachers) || empty($hotpot->studentfeedbackurl)) { + $hotpot->studentfeedback = HOTPOT_FEEDBACK_NONE; + } else { + $feedback[0] = "'$hotpot->studentfeedbackurl'"; + $feedback[1] = $teachers; + $feedback[2] = "'".fullname($USER)."'"; + $feedback[3] = "'".$USER->email."'"; + $feedback[4] = 500; // width + $feedback[5] = 300; // height + } + break; + + case HOTPOT_FEEDBACK_MOODLEFORUM: + $module = get_record('modules', 'name', 'forum'); + $forums = get_records('forum', 'course', "$course->id"); + if (empty($module) || empty($module->visible) || empty($forums)) { + $hotpot->studentfeedback = HOTPOT_FEEDBACK_NONE; + } else { + $feedback[0] = "'$CFG->wwwroot/mod/forum/index.php?id=$course->id'"; + } + break; + + case HOTPOT_FEEDBACK_MOODLEMESSAGING: + $teachers = hotpot_feedback_teachers($course, $hotpot); + if (empty($CFG->messaging) || empty($teachers)) { + $hotpot->studentfeedback = HOTPOT_FEEDBACK_NONE; + } else { + $feedback[0] = "'$CFG->wwwroot/message/discussion.php?id='"; + $feedback[1] = $teachers; + $feedback[4] = 400; // width + $feedback[5] = 500; // height + } + break; + + default: + // do nothing + } + + if ($hotpot->studentfeedback != HOTPOT_FEEDBACK_NONE) { + $feedback[6] = "'Send a message to teacher'"; + $feedback[7] = "'Title'"; + $feedback[8] = "'Teacher'"; + $feedback[9] = "'Message'"; + $feedback[10] = "'Close this window'"; + $js = ''; + foreach ($feedback as $i=>$str) { + $js .= 'FEEDBACK['.$i."] = $str;\n"; + } + $js = '\n"; + $hp->html = preg_replace('||i', "$js", $hp->html, 1); + } + // insert hot-potatoes.js $hp->insert_script(HOTPOT_JS); - // extract tag + // extract and tags $head = ''; - $pattern = '|^(.*)]*)>(.*?)|is'; - if (preg_match($pattern, $hp->html, $matches)) { - $head = $matches[3]; + $pattern = '|]*)>(.*?)|is'; + if (preg_match_all($pattern, $hp->html, $matches)) { + $last = count($matches[0])-1; + $head = $matches[2][$last]; + + // remove + $head = preg_replace('|<title[^>]*>(.*?)|is', '', $head); } // extract