From 212b7b8cd9af682fc32c76d1bfd6172ead62a27c Mon Sep 17 00:00:00 2001
From: tjhunt
Date: Tue, 22 Aug 2006 17:31:26 +0000
Subject: [PATCH] MDL-5270 - give students a feeback comment for their
performance in the entire quiz.
Also, along the way I noticed and fixed MDL-6290 student grades not rescaled when quiz maximum grade changed.
---
lang/en_utf8/help/quiz/overallfeedback.html | 14 +
lang/en_utf8/quiz.php | 8 +
mod/quiz/backuplib.php | 59 ++-
mod/quiz/db/mysql.php | 19 +
mod/quiz/db/mysql.sql | 10 +
mod/quiz/db/postgres7.php | 18 +
mod/quiz/db/postgres7.sql | 11 +-
mod/quiz/edit.php | 7 +-
mod/quiz/index.php | 9 +-
mod/quiz/lib.php | 110 ++++-
mod/quiz/locallib.php | 154 ++++++-
mod/quiz/mod.html | 77 +++-
mod/quiz/report/overview/report.php | 29 +-
mod/quiz/restorelib.php | 54 ++-
mod/quiz/review.php | 8 +-
mod/quiz/version.php | 2 +-
mod/quiz/view.php | 456 ++++++++++----------
17 files changed, 744 insertions(+), 301 deletions(-)
create mode 100644 lang/en_utf8/help/quiz/overallfeedback.html
diff --git a/lang/en_utf8/help/quiz/overallfeedback.html b/lang/en_utf8/help/quiz/overallfeedback.html
new file mode 100644
index 0000000000..5f8af1b65a
--- /dev/null
+++ b/lang/en_utf8/help/quiz/overallfeedback.html
@@ -0,0 +1,14 @@
+
Overall feedback
+
+
The overall feedback is some text that is shown to a student after
+they have completed an attempt at the quiz. The text that is shown
+can depend on the grade the student got.
+
+
For example, if you type "Well done" into the first feedback box, type 40%
+in the first grade boundary box, and type "Please study this week's work again"
+in the second feedback box, then students who score 40% or better will see the
+"Well done" message, and students who score less than 40% will see the other message.
+
+
The grade boundaries can be specified either as a percentage, for example "31.41%", or
+as a number, for example "7". If your quiz is out of 10 marks, a grade boundary of 7 means
+7/10 or better.
\ No newline at end of file
diff --git a/lang/en_utf8/quiz.php b/lang/en_utf8/quiz.php
index 38162bca45..423f5569fe 100644
--- a/lang/en_utf8/quiz.php
+++ b/lang/en_utf8/quiz.php
@@ -180,6 +180,11 @@ $string['exportnameformat'] = '%%Y%%m%%d-%%H%%M';
$string['exportquestions'] = 'Export questions to file';
$string['false'] = 'False';
$string['feedback'] = 'Feedback';
+$string['feedbackerrorboundaryformat'] = 'Feedback grade boundaries must be either a percentage or a number. The value you entered in boundary $a is not recognised.';
+$string['feedbackerrorboundaryoutofrange'] = 'Feedback grade boundaries must be between 0%% and 100%%. The value you entered in boundary $a is out of range.';
+$string['feedbackerrorjunkinboundary'] = 'You must fill in the feedback boxes without leaving any gaps.';
+$string['feedbackerrorjunkinfeedback'] = 'You must fill in the feedback boxes without leaving any gaps.';
+$string['feedbackerrororder'] = 'Feedback grade boundaries must in order, highest first. The value you entered in boundary $a is out of sequence.';
$string['file'] = 'File';
$string['fileformat'] = 'File format';
$string['fillcorrect'] = 'Fill with correct';
@@ -203,6 +208,7 @@ $string['geometric'] = 'Geometric';
$string['gift'] = 'GIFT format';
$string['grade'] = 'Grade';
$string['gradeaverage'] = 'Average grade';
+$string['gradeboundary'] = 'Grade boundary';
$string['gradehighest'] = 'Highest grade';
$string['grademethod'] = 'Grading method';
$string['gradingdetails'] = 'Marks for this submission: $a->raw/$a->max. ';
@@ -320,6 +326,7 @@ $string['onlyteachersimport'] = 'Only teachers with editing rights can import qu
$string['onlyteachersexport'] = 'Only teachers can export questions';
$string['optional'] = 'optional';
$string['outof'] = '$a->grade out of a maximum of $a->maxgrade';
+$string['overallfeedback'] = 'Overall feedback';
$string['overdue'] = 'Overdue';
$string['pagesize'] = 'Attempts shown per page: ';
$string['paragraphquestion'] = 'Paragraph Question not supported at line $a. The question will be ignored';
@@ -361,6 +368,7 @@ $string['quizcloses'] = 'Quiz closes';
$string['quiznotavailable'] = 'The quiz will not be available until: $a';
$string['quizopen'] = 'Open the quiz';
$string['quizopens'] = 'Quiz opens';
+$string['quizsettings'] = 'Quiz settings';
$string['quiztimelimit'] = 'Time limit: $a';
$string['quiztimer'] = 'Quiz Timer';
$string['pleaseclose'] = 'Your request has been processed. You can now close this window';
diff --git a/mod/quiz/backuplib.php b/mod/quiz/backuplib.php
index 5dd69b3267..19b91502ac 100644
--- a/mod/quiz/backuplib.php
+++ b/mod/quiz/backuplib.php
@@ -9,12 +9,12 @@
// (CL,pk->id)
// |
// -------------------------------------------------------------------
- // | | | |
- // | quiz_grades | quiz_question_versions
- // | (UL,pk->id,fk->quiz) | (CL,pk->id,fk->quiz)
- // | |
- // quiz_attempts quiz_question_instances
- // (UL,pk->id,fk->quiz) (CL,pk->id,fk->quiz,question)
+ // | | | | |
+ // | quiz_grades | quiz_question_versions |
+ // | (UL,pk->id,fk->quiz) | (CL,pk->id,fk->quiz) |
+ // | | |
+ // quiz_attempts quiz_question_instances quiz_feedback
+ // (UL,pk->id,fk->quiz) (CL,pk->id,fk->quiz,question) (CL,pk->id,fk->quiz)
//
// Meaning: pk->primary key field of the table
// fk->foreign key to link with parent
@@ -204,6 +204,8 @@
fwrite ($bf,full_tag("DELAY2",4,false,$quiz->delay2));
//Now we print to xml question_instances (Course Level)
$status = backup_quiz_question_instances($bf,$preferences,$quiz->id);
+ //Now we print to xml quiz_feedback (Course Level)
+ $status = backup_quiz_feedback($bf,$preferences,$quiz->id);
//Now we print to xml question_versions (Course Level)
$status = backup_quiz_question_versions($bf,$preferences,$quiz->id);
//if we've selected to backup users info, then execute:
@@ -242,9 +244,6 @@
//Backup quiz_question_instances contents (executed from quiz_backup_mods)
function backup_quiz_question_instances ($bf,$preferences,$quiz) {
-
- global $CFG;
-
$status = true;
$quiz_question_instances = get_records("quiz_question_instances","quiz",$quiz,"id");
@@ -269,11 +268,41 @@
return $status;
}
+ //Backup quiz_question_instances contents (executed from quiz_backup_mods)
+ function backup_quiz_feedback ($bf,$preferences,$quiz) {
+ $status = true;
+
+ $quiz_feedback = get_records('quiz_feedback', 'quizid', $quiz, 'id');
+ // If there are question_instances ...
+ if ($quiz_feedback) {
+ // Write start tag.
+ $status = $status & fwrite($bf,start_tag('FEEDBACKS', 4, true));
+
+ // Iterate over each question_instance.
+ foreach ($quiz_feedback as $feedback) {
+
+ //Start feedback instance
+ $status = $status & fwrite($bf, start_tag('FEEDBACK',5,true));
+
+ //Print question_instance contents.
+ $status = $status & fwrite($bf, full_tag('ID', 6, false, $feedback->id));
+ $status = $status & fwrite($bf, full_tag('QUIZID', 6, false, $feedback->quizid));
+ $status = $status & fwrite($bf, full_tag('FEEDBACKTEXT', 6, false, $feedback->feedbacktext));
+ $status = $status & fwrite($bf, full_tag('MINGRADE', 6, false, $feedback->mingrade));
+ $status = $status & fwrite($bf, full_tag('MAXGRADE', 6, false, $feedback->maxgrade));
+
+ // End feedback instance.
+ $status = $status & fwrite($bf, end_tag('FEEDBACK', 5, true));
+ }
+
+ // Write end tag.
+ $status = $status & fwrite($bf, end_tag('FEEDBACKS', 4, true));
+ }
+ return $status;
+ }
+
//Backup quiz_question_versions contents (executed from quiz_backup_mods)
function backup_quiz_question_versions ($bf,$preferences,$quiz) {
-
- global $CFG;
-
$status = true;
$quiz_question_versions = get_records("quiz_question_versions","quiz",$quiz,"id");
@@ -304,9 +333,6 @@
//Backup quiz_grades contents (executed from quiz_backup_mods)
function backup_quiz_grades ($bf,$preferences,$quiz) {
-
- global $CFG;
-
$status = true;
$quiz_grades = get_records("quiz_grades","quiz",$quiz,"id");
@@ -334,9 +360,6 @@
//Backup quiz_attempts contents (executed from quiz_backup_mods)
function backup_quiz_attempts ($bf,$preferences,$quiz) {
-
- global $CFG;
-
$status = true;
$quiz_attempts = get_records("quiz_attempts","quiz",$quiz,"id");
diff --git a/mod/quiz/db/mysql.php b/mod/quiz/db/mysql.php
index e801cec6df..dec47e95f7 100644
--- a/mod/quiz/db/mysql.php
+++ b/mod/quiz/db/mysql.php
@@ -1113,6 +1113,25 @@ function quiz_upgrade($oldversion) {
(($CFG->quiz_review & QUIZ_REVIEW_FEEDBACK) << 3));
}
+ if ($success && $oldversion < 2006081400) {
+ $success = $success && modify_database('', "
+ CREATE TABLE prefix_quiz_feedback (
+ id int(10) unsigned NOT NULL auto_increment,
+ quizid int(10) unsigned NOT NULL default '0',
+ feedbacktext text NOT NULL default '',
+ mingrade double NOT NULL default '0',
+ maxgrade double NOT NULL default '0',
+ PRIMARY KEY (id),
+ KEY quizid (quizid)
+ ) TYPE=MyISAM COMMENT='Feedback given to students based on their overall score on the test';
+ ");
+
+ $success = $success && execute_sql("
+ INSERT INTO {$CFG->prefix}quiz_feedback (quizid, feedbacktext, maxgrade, mingrade)
+ SELECT id, '', grade + 1, 0 FROM {$CFG->prefix}quiz;
+ ");
+ }
+
return $success;
}
diff --git a/mod/quiz/db/mysql.sql b/mod/quiz/db/mysql.sql
index 9d603a85fd..3db85ed754 100644
--- a/mod/quiz/db/mysql.sql
+++ b/mod/quiz/db/mysql.sql
@@ -62,6 +62,16 @@ CREATE TABLE prefix_quiz_question_versions (
PRIMARY KEY (id)
) TYPE=MyISAM COMMENT='The mapping between old and new versions of a question';
+CREATE TABLE prefix_quiz_feedback (
+ id int(10) unsigned NOT NULL auto_increment,
+ quizid int(10) unsigned NOT NULL default '0',
+ feedbacktext text NOT NULL default '',
+ mingrade double NOT NULL default '0',
+ maxgrade double NOT NULL default '0',
+ PRIMARY KEY (id),
+ KEY quizid (quizid),
+) TYPE=MyISAM COMMENT='Feedback given to students based on their overall score on the test';
+
-- --------------------------------------------------------
-- Quiz module, quiz runtime data.
-- --------------------------------------------------------
diff --git a/mod/quiz/db/postgres7.php b/mod/quiz/db/postgres7.php
index b02028342e..aeb160af8d 100644
--- a/mod/quiz/db/postgres7.php
+++ b/mod/quiz/db/postgres7.php
@@ -1435,6 +1435,24 @@ function quiz_upgrade($oldversion) {
(($CFG->quiz_review & QUIZ_REVIEW_FEEDBACK) << 3));
}
+ if ($success && $oldversion < 2006081400) {
+ $success = $success && modify_database('', "
+ CREATE TABLE prefix_quiz_feedback (
+ id SERIAL PRIMARY KEY,
+ quizid integer NOT NULL default '0',
+ feedbacktext text NOT NULL default '',
+ maxgrade real NOT NULL default '0',
+ mingrade real NOT NULL default '0'
+ );
+ ");
+ $success = $success && modify_database('',
+ "CREATE INDEX prefix_quiz_feedback_quizid_idx ON prefix_quiz_feedback (quizid);");
+
+ $success = $success && execute_sql("
+ INSERT INTO {$CFG->prefix}quiz_feedback (quizid, feedbacktext, maxgrade, mingrade)
+ SELECT id, '', grade + 1, 0 FROM {$CFG->prefix}quiz;
+ ");
+ }
return $success;
}
diff --git a/mod/quiz/db/postgres7.sql b/mod/quiz/db/postgres7.sql
index 9f7528b292..5f43a8fa26 100644
--- a/mod/quiz/db/postgres7.sql
+++ b/mod/quiz/db/postgres7.sql
@@ -59,6 +59,15 @@ CREATE TABLE prefix_quiz_question_versions (
timestamp integer NOT NULL default '0'
);
+CREATE TABLE prefix_quiz_feedback (
+ id SERIAL PRIMARY KEY,
+ quizid integer NOT NULL default '0',
+ feedbacktext text NOT NULL default '',
+ mingrade real NOT NULL default '0',
+ maxgrade real NOT NULL default '0'
+);
+CREATE INDEX prefix_quiz_feedback_quizid_idx ON prefix_quiz_feedback (quizid);
+
-- --------------------------------------------------------
-- Quiz module, quiz runtime data.
-- --------------------------------------------------------
@@ -207,7 +216,7 @@ CREATE TABLE prefix_question_states (
penalty real NOT NULL default '0'
);
CREATE INDEX prefix_question_states_attempt_idx ON prefix_question_states (attempt);
-CREATE INDEX prefix_question_states_question_idx ON prefix_question_states (question);;
+CREATE INDEX prefix_question_states_question_idx ON prefix_question_states (question);
-- --------------------------------------------------------
-- Quiz log actions.
diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php
index 5109cafe89..e2d0d0db93 100644
--- a/mod/quiz/edit.php
+++ b/mod/quiz/edit.php
@@ -274,9 +274,8 @@ if (self.name == 'editquestion') {
// If rescaling is required save the new maximum
if (isset($_REQUEST['maxgrade'])) {
- $modform->grade = optional_param('maxgrade', 0);
- if (!set_field('quiz', 'grade', $modform->grade, 'id', $modform->instance)) {
- error('Could not set new maximal grade for quiz');
+ if (!quiz_set_grade(optional_param('maxgrade', 0), $modform)) {
+ error('Could not set a new maximum grade for the quiz');
}
}
}
@@ -308,7 +307,7 @@ if (self.name == 'editquestion') {
// Print basic page layout.
- if (isset($modform->instance) and record_exists_sql("SELECT * FROM {$CFG->prefix}quiz_attempts WHERE quiz = '$modform->instance' AND preview = '0' LIMIT 1")){
+ if (isset($modform->instance) and record_exists_select('quiz_attempts', "quiz = '$modform->instance' AND preview = '0'")){
// one column layout with table of questions used in this quiz
$strupdatemodule = has_capability('moodle/course:manageactivities', $coursecontext)
? update_module_button($modform->cmid, $course->id, get_string('modulename', 'quiz'))
diff --git a/mod/quiz/index.php b/mod/quiz/index.php
index 33dce3e16e..3494c9b260 100644
--- a/mod/quiz/index.php
+++ b/mod/quiz/index.php
@@ -121,20 +121,23 @@
// or the quiz has no grade, display nothing in grade col
if ($bestgrade === NULL || $quiz->grade == 0) {
$gradecol = "";
+ $feedbackcol = '';
} else {
//If all quiz's attempts have visible results, show bestgrade
if(all_attempt_results_visible($quiz, $USER)) {
$gradecol = "$bestgrade / $quiz->grade";
+ $feedbackcol = quiz_get_feedback($quiz, $bestgrade);
} else {
$gradecol = "";
+ $feedbackcol = '';
}
}
}
if ($course->format == "weeks" or $course->format == "topics") {
- $table->data[] = array ($printsection, $link, $closequiz, $gradecol);
+ $table->data[] = array ($printsection, $link, $closequiz, $gradecol, $feedbackcol);
} else {
- $table->data[] = array ($link, $closequiz, $gradecol);
+ $table->data[] = array ($link, $closequiz, $gradecol, $feedbackcol);
}
}
@@ -146,4 +149,4 @@
print_footer($course);
-?>
+?>
\ No newline at end of file
diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php
index 976fa56170..0b38d223bb 100644
--- a/mod/quiz/lib.php
+++ b/mod/quiz/lib.php
@@ -58,14 +58,18 @@ define("QUIZ_MAX_EVENT_LENGTH", "432000"); // 5 days maximum
* of the new instance.
*
* @param object $quiz the data that came from the form.
- * @return integer the id of the new instance.
+ * @return mixed the id of the new instance on success,
+ * false or a string error message on failure.
*/
function quiz_add_instance($quiz) {
// Process the options from the form.
$quiz->created = time();
- quiz_process_options($quiz);
$quiz->questions = '';
+ $result = quiz_process_options($quiz);
+ if ($result && is_string($result)) {
+ return $result;
+ }
// Try to store it in the database.
if (!$quiz->id = insert_record("quiz", $quiz)) {
@@ -84,12 +88,15 @@ function quiz_add_instance($quiz) {
* will update an existing instance with new data.
*
* @param object $quiz the data that came from the form.
- * @return boolean true on success, false on failure.
+ * @return mixed true on success, false or a string error message on failure.
*/
function quiz_update_instance($quiz) {
// Process the options from the form.
- quiz_process_options($quiz);
+ $result = quiz_process_options($quiz);
+ if ($result && is_string($result)) {
+ return $result;
+ }
// Update the database.
$quiz->id = $quiz->instance;
@@ -120,7 +127,7 @@ function quiz_delete_instance($id) {
if ($attempts = get_records("quiz_attempts", "quiz", "$quiz->id")) {
foreach ($attempts as $attempt) {
- // TODO: this should use function in questionlib.php
+ // TODO: this should use the delete_attempt($attempt->uniqueid) function in questionlib.php
if (! delete_records("question_states", "attempt", "$attempt->uniqueid")) {
$result = false;
}
@@ -130,20 +137,18 @@ function quiz_delete_instance($id) {
}
}
- if (! delete_records("quiz_attempts", "quiz", "$quiz->id")) {
- $result = false;
- }
-
- if (! delete_records("quiz_grades", "quiz", "$quiz->id")) {
- $result = false;
- }
-
- if (! delete_records("quiz_question_instances", "quiz", "$quiz->id")) {
- $result = false;
- }
-
- if (! delete_records("quiz", "id", "$quiz->id")) {
- $result = false;
+ $tables_to_purge = array(
+ 'quiz_attempts' => 'quiz',
+ 'quiz_grades' => 'quiz',
+ 'quiz_question_instances' => 'quiz',
+ 'quiz_grades' => 'quiz',
+ 'quiz_feedback' => 'quizid',
+ 'quiz' => 'id'
+ );
+ foreach ($tables_to_purge as $table => $keyfield) {
+ if (!delete_records($table, $keyfield, $quiz->id)) {
+ $result = false;
+ }
}
$pagetypes = page_import_types('mod/quiz/');
@@ -495,7 +500,58 @@ function quiz_process_options(&$quiz) {
$quiz->timelimit = 0;
}
$quiz->timelimit = round($quiz->timelimit);
-
+
+ // Quiz feedback
+
+ // Clean up the boundary text.
+ for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
+ if (empty($quiz->feedbacktext[$i])) {
+ $quiz->feedbacktext[$i] = '';
+ } else {
+ $quiz->feedbacktext[$i] = trim($quiz->feedbacktext[$i]);
+ }
+ }
+
+ // Check the boundary value is a number or a percentage, and in range.
+ $i = 0;
+ while (!empty($quiz->feedbackboundaries[$i])) {
+ $boundary = trim($quiz->feedbackboundaries[$i]);
+ if (!is_numeric($boundary)) {
+ if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
+ $boundary = substr($boundary, 0, -1);
+ if (is_numeric($boundary)) {
+ $boundary = $boundary * $quiz->grade / 100.0;
+ } else {
+ return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
+ }
+ }
+ }
+ if ($boundary <= 0 || $boundary >= $quiz->grade) {
+ return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
+ }
+ if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
+ return get_string('feedbackerrororder', 'quiz', $i + 1);
+ }
+ $quiz->feedbackboundaries[$i] = $boundary;
+ $i += 1;
+ }
+ $numboundaries = $i;
+
+ // Check there is nothing in the remaining unused fields.
+ for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
+ if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
+ return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
+ }
+ }
+ for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
+ if (!empty($quiz->feedbacktext[$i]) && trim($quiz->feedbacktext[$i]) != '') {
+ return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
+ }
+ }
+ $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
+ $quiz->feedbackboundaries[$numboundaries] = 0;
+ $quiz->feedbackboundarycount = $numboundaries;
+
// Settings that get combined to go into the optionflags column.
$quiz->optionflags = 0;
if (!empty($quiz->adaptive)) {
@@ -593,6 +649,20 @@ function quiz_process_options(&$quiz) {
*/
function quiz_after_add_or_update($quiz) {
+ // Save the feedback
+ delete_records('quiz_feedback', 'quizid', $quiz->id);
+
+ for ($i = 0; $i <= $quiz->feedbackboundarycount; $i += 1) {
+ $feedback = new stdClass;
+ $feedback->quizid = $quiz->id;
+ $feedback->feedbacktext = $quiz->feedbacktext[$i];
+ $feedback->mingrade = $quiz->feedbackboundaries[$i];
+ $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
+ if (!insert_record('quiz_feedback', $feedback, false)) {
+ return "Could not save quiz feedback.";
+ }
+ }
+
// Remember whether this user likes the advanced settings visible or hidden.
if (isset($quiz->optionsettingspref)) {
set_user_preference('quiz_optionsettingspref', $quiz->optionsettingspref);
diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php
index 241f1f31eb..d69f186f0c 100644
--- a/mod/quiz/locallib.php
+++ b/mod/quiz/locallib.php
@@ -83,10 +83,18 @@ function quiz_get_user_attempt_unfinished($quizid, $userid) {
return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0);
}
+/**
+ * @param integer $quizid the quiz id.
+ * @param integer $userid the userid.
+ * @return an array of all the ueser's attempts at this quiz. Returns an empty array if there are none.
+ */
function quiz_get_user_attempts($quizid, $userid) {
-// Returns a list of all attempts by a user
- return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0",
- "attempt ASC");
+ if ($attempts = get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0",
+ "attempt ASC")) {
+ return $attempts;
+ } else {
+ return array();
+ }
}
@@ -249,30 +257,132 @@ function quiz_get_all_question_grades($quiz) {
return $grades;
}
-
+/**
+ * Get the best current grade for a particular user in a quiz.
+ *
+ * @param object $quiz the quiz object.
+ * @param integer $userid the id of the user.
+ * @return float the user's current grade for this quiz.
+ */
function quiz_get_best_grade($quiz, $userid) {
-/// Get the best current grade for a particular user in a quiz
-if (!$grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
+ $grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id, 'userid', $userid);
+
+ // Need to detect errors/no result, without catching 0 scores.
+ if (is_numeric($grade)) {
+ return round($grade,$quiz->decimalpoints);
+ } else {
return NULL;
}
+}
- return (round($grade->grade,$quiz->decimalpoints));
+/**
+ * Convert the raw grade stored in $attempt into a grade out of the maximum
+ * grade for this quiz.
+ *
+ * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
+ * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
+ * @return float the rescaled grade.
+ */
+function quiz_rescale_grade($rawgrade, $quiz) {
+ if ($quiz->sumgrades) {
+ return round($rawgrade*$quiz->grade/$quiz->sumgrades, $quiz->decimalpoints);
+ } else {
+ return 0;
+ }
}
/**
-* Save the overall grade for a user at a quiz in the quiz_grades table
-*
-* @return boolean Indicates success or failure.
-* @param object $quiz The quiz for which the best grade is to be calculated
-* and then saved.
-* @param integer $userid The id of the user to save the best grade for. Can be
-* null in which case the current user is assumed.
-*/
-function quiz_save_best_grade($quiz, $userid=null) {
+ * Get the feedback text that should be show to a student who
+ * got this grade on this quiz.
+ *
+ * @param float $grade a grade on this quiz.
+ * @param integer $quizid the id of the quiz object.
+ * @return string the comment that corresponds to this grade (empty string if there is not one.
+ */
+function quiz_feedback_for_grade($grade, $quizid) {
+ $feedback = get_field_select('quiz_feedback', 'feedbacktext',
+ "quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade");
+
+ if (empty($feedback)) {
+ $feedback = '';
+ }
+
+ return $feedback;
+}
+
+/**
+ * @param integer $quizid the id of the quiz object.
+ * @return boolean Whether this quiz has any non-blank feedback text.
+ */
+function quiz_has_feedback($quizid) {
+ static $cache = array();
+ if (!array_key_exists($quizid, $cache)) {
+ $cache[$quizid] = record_exists_select('quiz_feedback',
+ "quizid = $quizid AND feedbacktext <> ''");
+ }
+ return $cache[$quizid];
+}
+
+/**
+ * The quiz grade is the score that student's results are marked out of. When it
+ * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
+ * rescaled.
+ *
+ * @param float $newgrade the new maximum grade for the quiz.
+ * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
+ * @return boolean indicating success or failure.
+ */
+function quiz_set_grade($newgrade, &$quiz) {
+ // This is potentially expensive, so only do it if necessary.
+ if (abs($quiz->grade - $newgrade) < 1e-7) {
+ // Nothing to do.
+ return true;
+ }
+
+ // Use a transaction, so that on those databases that support it, this is safer.
+ begin_sql();
+
+ // Update the quiz table.
+ $success = set_field('quiz', 'grade', $newgrade, 'id', $quiz->instance);
+
+ // Rescaling the other data is only possible if the old grade was non-zero.
+ if ($quiz->grade > 1e-7) {
+ global $CFG;
+
+ $factor = $newgrade/$quiz->grade;
+ $quiz->grade = $newgrade;
+
+ // Update the quiz_grades table.
+ $timemodified = time();
+ $success = $success && set_field('quiz_grades', 'grade',
+ '$factor * grade, timemodified = $timemodified',
+ 'quiz', $quiz->id);
+
+ // Update the quiz_grades table.
+ $success = $success && execute_sql('quiz_feedback', 'mingrade',
+ '$factor * mingrade, maxgrade = $factor * maxgrade',
+ 'quizid', $quiz->id);
+ }
+
+ if ($success) {
+ return commit_sql();
+ } else {
+ rollback_sql();
+ return false;
+ }
+}
+
+/**
+ * Save the overall grade for a user at a quiz in the quiz_grades table
+ *
+ * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
+ * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
+ * @return boolean Indicates success or failure.
+ */
+function quiz_save_best_grade($quiz, $userid = null) {
global $USER;
- // Assume the current user if $userid is null
- if (is_null($userid)) {
+ if (empty($userid)) {
$userid = $USER->id;
}
@@ -284,12 +394,10 @@ function quiz_save_best_grade($quiz, $userid=null) {
// Calculate the best grade
$bestgrade = quiz_calculate_best_grade($quiz, $attempts);
- $bestgrade = $quiz->sumgrades ? (($bestgrade / $quiz->sumgrades) * $quiz->grade) : 0;
- $bestgrade = round($bestgrade, $quiz->decimalpoints);
-
+ $bestgrade = quiz_rescale_grade($bestgrade, $quiz);
+
// Save the best grade in the database
- if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid',
- $userid)) {
+ if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
$grade->grade = $bestgrade;
$grade->timemodified = time();
if (!update_record('quiz_grades', $grade)) {
diff --git a/mod/quiz/mod.html b/mod/quiz/mod.html
index abe9e2323f..7544616c69 100644
--- a/mod/quiz/mod.html
+++ b/mod/quiz/mod.html
@@ -1,7 +1,7 @@
-
-
-
+// It is used from /course/mod.php. The whole instance is available as $form. -->
+
require_once("$CFG->dirroot/mod/quiz/locallib.php");
// Set any form variables that have not been initialized to their default value.
@@ -77,6 +77,37 @@
if (!isset($form->delay2)) {
$form->delay2 = $CFG->quiz_delay2;
}
+
+ // Get any existing feedback text out of the database.
+ if (!empty($form->id)) {
+ $feedbacks = get_records('quiz_feedback', 'quizid', $form->id, 'mingrade DESC');
+ } else {
+ $feedbacks = array();
+ }
+ $form->feedbacktext = array();
+ $form->feedbackboundaries = array();
+ foreach ($feedbacks as $feedback) {
+ $form->feedbacktext[] = $feedback->feedbacktext;
+ if ($feedback->mingrade > 0) {
+ $form->feedbackboundaries[] = (100.0 * $feedback->mingrade / $form->grade) . '%';
+ }
+ }
+
+ // Make sure there are at least 5 feedbacktexts, or a bit more than the current nubmer.
+ $numfeedbacks = max(
+ count($form->feedbacktext) * 1.5,
+ count($form->feedbackboundaries) * 1.5,
+ 5
+ );
+
+ for ($i = 0; $i < $numfeedbacks; $i += 1) {
+ if (!array_key_exists($i, $form->feedbacktext)) {
+ $form->feedbacktext[$i] = '';
+ }
+ if (!array_key_exists($i, $form->feedbackboundaries)) {
+ $form->feedbackboundaries[$i] = '';
+ }
+ }
// The following are used for drop-down menus
$yesnooptions = array(get_string("no"), get_string("yes"));
@@ -186,11 +217,47 @@
// This time out put the ones that were not fixed.
$fix = output_quiz_options_fields($form, 0);
+ // Output standard module settings.
print_standard_coursemodule_settings($form);
+ // Output the boxes for typing feedback depending on overall quiz score.
+?>
+
+
+
+
+
+
:
+
100%
+
+
+feedbacktext); $i = $i + 1) { ?>
+
+
+
:
+
+
+
+
+
+feedbacktext) - 1) { ?>
+
+
:
+
+
+
+
+
+
+
+
+
:
+
0%
+
+
+
:
@@ -499,4 +566,4 @@ function output_quiz_options_fields($form, $showfixed) {
+?>
\ No newline at end of file
diff --git a/mod/quiz/report/overview/report.php b/mod/quiz/report/overview/report.php
index beca8a4fd6..c618b580ad 100644
--- a/mod/quiz/report/overview/report.php
+++ b/mod/quiz/report/overview/report.php
@@ -88,6 +88,7 @@ class quiz_report extends quiz_default_report {
$noattempts = optional_param('noattempts', 0, PARAM_INT);
$detailedmarks = optional_param('detailedmarks', 0, PARAM_INT);
$pagesize = optional_param('pagesize', 10, PARAM_INT);
+ $hasfeedback = quiz_has_feedback($quiz->id) && $quiz->grade > 1.e-7 && $quiz->sumgrades > 1.e-7;
// Now check if asked download of data
if ($download) {
@@ -134,6 +135,11 @@ class quiz_report extends quiz_default_report {
}
}
+ if ($hasfeedback) {
+ $tablecolumns[] = 'feedback';
+ $tableheaders[] = get_string('feedback', 'quiz');
+ }
+
if (!$download) {
// Set up the table
@@ -200,6 +206,9 @@ class quiz_report extends quiz_default_report {
$headers[] = '#'.$questions[$id]->number;
}
}
+ if ($hasfeedback) {
+ $headers[] = get_string('feedback', 'quiz');
+ }
$colnum = 0;
foreach ($headers as $item) {
$myxls->write(0,$colnum,$item,$formatbc);
@@ -225,6 +234,9 @@ class quiz_report extends quiz_default_report {
$headers .= "\t#".$question->number;
}
}
+ if ($hasfeedback) {
+ $headers .= "\t" . get_string('feedback', 'quiz');
+ }
echo $headers." \n";
}
@@ -319,7 +331,7 @@ class quiz_report extends quiz_default_report {
if (empty($sort)) {
$sort = ' ORDER BY uniqueid';
}
-
+
// Now it is time to page the data
if (!isset($pagesize) || ((int)$pagesize < 1) ) {
$pagesize = 10;
@@ -333,6 +345,14 @@ class quiz_report extends quiz_default_report {
}
}
+ // If there is feedback, include it in the query.
+ if ($hasfeedback) {
+ $factor = $quiz->grade/$quiz->sumgrades;
+ $select .= ', qf.feedbacktext ';
+ $from .= " JOIN {$CFG->prefix}quiz_feedback AS qf ON " .
+ "qf.quizid = $quiz->id AND qf.mingrade <= qa.sumgrades * $factor AND qa.sumgrades * $factor < qf.maxgrade";
+ }
+
// Fetch the attempts
if (!empty($from)) { // if we're in the site course and displaying no attempts, it makes no sense to do the query.
$attempts = get_records_sql($select.$from.$where.$sort.$limit);
@@ -403,6 +423,13 @@ class quiz_report extends quiz_default_report {
}
}
}
+ if ($hasfeedback) {
+ if ($attempt->timefinish) {
+ $row[] = $attempt->feedbacktext;
+ } else {
+ $row[] = '-';
+ }
+ }
if (!$download) {
$table->add_data($row);
} else if ($download == 'Excel') {
diff --git a/mod/quiz/restorelib.php b/mod/quiz/restorelib.php
index c001f2bae0..179f5c1bf5 100644
--- a/mod/quiz/restorelib.php
+++ b/mod/quiz/restorelib.php
@@ -103,6 +103,8 @@
$mod->id, $newid);
//We have to restore the question_instances now (course level table)
$status = quiz_question_instances_restore_mods($newid,$info,$restore);
+ //We have to restore the feedback now (course level table)
+ $status = quiz_feedback_restore_mods($newid, $info, $restore, $quiz);
//We have to restore the question_versions now (course level table)
$status = quiz_question_versions_restore_mods($newid,$info,$restore);
//Now check if want to restore user data and do it.
@@ -132,7 +134,11 @@
$status = true;
//Get the quiz_question_instances array
- $instances = $info['MOD']['#']['QUESTION_INSTANCES']['0']['#']['QUESTION_INSTANCE'];
+ if (array_key_exists('QUESTION_INSTANCES', $info['MOD']['#'])) {
+ $instances = $info['MOD']['#']['QUESTION_INSTANCES']['0']['#']['QUESTION_INSTANCE'];
+ } else {
+ $instances = array();
+ }
//Iterate over question_instances
for($i = 0; $i < sizeof($instances); $i++) {
@@ -181,6 +187,52 @@
return $status;
}
+ //This function restores the quiz_question_instances
+ function quiz_feedback_restore_mods($quiz_id, $info, $restore, $quiz) {
+ $status = true;
+
+ //Get the quiz_feedback array
+ if (array_key_exists('FEEDBACKS', $info['MOD']['#'])) {
+ $feedbacks = $info['MOD']['#']['FEEDBACKS']['0']['#']['FEEDBACK'];
+
+ //Iterate over the feedbacks
+ foreach ($feedbacks as $feedback_info) {
+ //traverse_xmlize($feedback_info); //Debug
+ //print_object ($GLOBALS['traverse_array']); //Debug
+ //$GLOBALS['traverse_array']=""; //Debug
+
+ //We'll need this later!!
+ $oldid = backup_todb($feedback_info['#']['ID']['0']['#']);
+
+ //Now, build the quiz_feedback record structure
+ $feedback = new stdClass();
+ $feedback->quizid = $quiz_id;
+ $feedback->feedbacktext = backup_todb($feedback_info['#']['FEEDBACKTEXT']['0']['#']);
+ $feedback->mingrade = backup_todb($feedback_info['#']['MINGRADE']['0']['#']);
+ $feedback->maxgrade = backup_todb($feedback_info['#']['MAXGRADE']['0']['#']);
+
+ //The structure is equal to the db, so insert the quiz_question_instances
+ $newid = insert_record('quiz_feedback', $feedback);
+
+ if ($newid) {
+ //We have the newid, update backup_ids
+ backup_putid($restore->backup_unique_code, 'quiz_feedback', $oldid, $newid);
+ } else {
+ $status = false;
+ }
+ }
+ } else {
+ $feedback = new stdClass();
+ $feedback->quizid = $quiz_id;
+ $feedback->feedbacktext = '';
+ $feedback->mingrade = 0;
+ $feedback->maxgrade = $quiz->grade + 1;
+ insert_record('quiz_feedback', $feedback);
+ }
+
+ return $status;
+ }
+
//This function restores the quiz_question_versions
function quiz_question_versions_restore_mods($quiz_id,$info,$restore) {
diff --git a/mod/quiz/review.php b/mod/quiz/review.php
index 7905d24407..41e31c868f 100644
--- a/mod/quiz/review.php
+++ b/mod/quiz/review.php
@@ -30,6 +30,9 @@
if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
error("The course module for the quiz with id $quiz->id is missing");
}
+
+ $grade = quiz_rescale_grade($attempt->sumgrades, $quiz);
+ $feedback = quiz_feedback_for_grade($grade, $attempt->quiz);
if (!count_records('question_sessions', 'attemptid', $attempt->uniqueid)) {
// this question has not yet been upgraded to the new model
@@ -194,13 +197,16 @@
$a = new stdClass;
$percentage = round(($attempt->sumgrades/$quiz->sumgrades)*100, 0);
- $a->grade = round(($attempt->sumgrades/$quiz->sumgrades)*$quiz->grade, $CFG->quiz_decimalpoints);
+ $a->grade = $grade;
$a->maxgrade = $quiz->grade;
$rawscore = round($attempt->sumgrades, $CFG->quiz_decimalpoints);
$table->data[] = array("$strscore:", "$rawscore/$quiz->sumgrades ($percentage %)");
$table->data[] = array("$strgrade:", get_string('outof', 'quiz', $a));
}
}
+ if ($options->feedback && $feedback) {
+ $table->data[] = array(get_string('feedback', 'quiz'), $feedback);
+ }
if ($isteacher and $attempt->userid == $USER->id) {
// the teacher is at the end of a preview. Print button to start new preview
unset($buttonoptions);
diff --git a/mod/quiz/version.php b/mod/quiz/version.php
index eefc0afbfc..5d7d53dcce 100644
--- a/mod/quiz/version.php
+++ b/mod/quiz/version.php
@@ -5,7 +5,7 @@
// This fragment is called by moodle_needs_upgrading() and /admin/index.php
////////////////////////////////////////////////////////////////////////////////
-$module->version = 2006081000; // The (date) version of this module
+$module->version = 2006081400; // The (date) version of this module
$module->requires = 2006080900; // Requires this Moodle version
$module->cron = 0; // How often should cron check this module (seconds)?
diff --git a/mod/quiz/view.php b/mod/quiz/view.php
index 2219f4c4af..ef34ea0730 100644
--- a/mod/quiz/view.php
+++ b/mod/quiz/view.php
@@ -48,15 +48,13 @@
$timenow = time();
-// Initialize $PAGE, compute blocks
-
+ // Initialize $PAGE, compute blocks
$PAGE = page_create_instance($quiz->id);
$pageblocks = blocks_setup($PAGE);
$blocks_preferred_width = bounded_number(180, blocks_preferred_width($pageblocks[BLOCK_POS_LEFT]), 210);
-// Print the page header
-
- if (($edit != -1) and $PAGE->user_allowed_editing()) {
+ // Print the page header
+ if ($edit != -1 and $PAGE->user_allowed_editing()) {
$USER->editing = $edit;
}
@@ -77,13 +75,15 @@
$available = ($quiz->timeopen < $timenow and ($timenow < $quiz->timeclose or !$quiz->timeclose)) || $isteacher;
-// Print the main part of the page
+ // Print the main part of the page
// Print heading and tabs for teacher
if ($isteacher) {
$currenttab = 'info';
include('tabs.php');
}
+
+ // Print quiz name and description.
print_heading(format_string($quiz->name));
if (trim(strip_tags($quiz->intro))) {
@@ -91,12 +91,15 @@
print_simple_box(format_text($quiz->intro, FORMAT_MOODLE, $formatoptions), "center");
}
+ // Print information about number of attempts and grading method.
if ($quiz->attempts > 1) {
echo "