2005-04-03 00:19:11 +00:00

707 lines
27 KiB
PHP

<?php // $Id$
/// Overview report just displays a big table of all the attempts
////////////////////////////////////////////////////////////////
/// With the refactoring of the quiz module in July-2004, some
/// of the functions in lib.php were moved here instead as they
/// are no longer in use by the other quiz components.
/// These functions are quiz_get_attempt_responses,
/// quiz_grade_attempt, quiz_grade_attempt_results,
/// quiz_remove_unwanted_questions and quiz_get_answers.
/// They were all properly renamed by exchanging quiz_
/// with quiz_report_simplestat_
//////////////////////////////////////////////////////////
function quiz_report_simplestat_get_attempt_responses($attempt) {
// Given an attempt object, this function gets all the
// stored responses and returns them in a format suitable
// for regrading using quiz_grade_attempt_results()
global $CFG;
if (!$responses = get_records_sql("SELECT q.id, q.qtype, q.category, q.questiontext,
q.defaultgrade, q.image, r.answer
FROM {$CFG->prefix}quiz_responses r,
{$CFG->prefix}quiz_questions q
WHERE r.attempt = '$attempt->id'
AND q.id = r.question")) {
notify("Could not find any responses for that attempt!");
return false;
}
foreach ($responses as $key => $response) {
if ($response->qtype == RANDOM) {
$responses[$key]->random = $response->answer;
$responses[$response->answer]->delete = true;
$realanswer = $responses[$response->answer]->answer;
if (is_array($realanswer)) {
$responses[$key]->answer = $realanswer;
} else {
$responses[$key]->answer = explode(",", $realanswer);
}
} else if ($response->qtype == NUMERICAL or $response->qtype == SHORTANSWER) {
$responses[$key]->answer = array($response->answer);
} else {
$responses[$key]->answer = explode(",",$response->answer);
}
}
foreach ($responses as $key => $response) {
if (!empty($response->delete)) {
unset($responses[$key]);
}
}
return $responses;
}
function quiz_report_simplestat_grade_attempt_question_result($question,
$answers,
$gradecanbenegative= false)
{
$grade = 0; // default
$correct = array();
$feedback = array();
$response = array();
switch ($question->qtype) {
case SHORTANSWER:
if ($question->answer) {
$question->answer = trim(stripslashes($question->answer[0]));
} else {
$question->answer = "";
}
$response[0] = $question->answer;
$feedback[0] = ''; // Default
foreach ($answers as $answer) { // There might be multiple right answers
$answer->answer = trim($answer->answer); // Just in case
if ($answer->fraction >= 1.0) {
$correct[] = $answer->answer;
}
if (!$answer->usecase) { // Don't compare case
$answer->answer = strtolower($answer->answer);
$question->answer = strtolower($question->answer);
}
$potentialgrade = (float)$answer->fraction * $question->grade;
if ($potentialgrade >= $grade and (strpos(' '.$answer->answer, '*'))) {
$answer->answer = str_replace('\*','@@@@@@',$answer->answer);
$answer->answer = str_replace('*','.*',$answer->answer);
$answer->answer = str_replace('@@@@@@', '\*',$answer->answer);
$answer->answer = str_replace('+', '\+',$answer->answer);
if (eregi('^'.$answer->answer.'$', $question->answer)) {
$feedback[0] = $answer->feedback;
$grade = $potentialgrade;
}
} else if ($answer->answer == $question->answer) {
$feedback[0] = $answer->feedback;
$grade = $potentialgrade;
}
}
break;
case NUMERICAL:
if ($question->answer) {
$question->answer = trim(stripslashes($question->answer[0]));
} else {
$question->answer = "";
}
$response[0] = $question->answer;
$bestshortanswer = 0;
foreach ($answers as $answer) { // There might be multiple right answers
if ($answer->fraction > $bestshortanswer) {
$correct[$answer->id] = $answer->answer;
$bestshortanswer = $answer->fraction;
$feedback[0] = $answer->feedback; // Show feedback for best answer
}
if ('' != $question->answer // Must not be mixed up with zero!
&& (float)$answer->fraction > (float)$grade // Do we need to bother?
and // and has lower procedence than && and ||.
strtolower($question->answer) == strtolower($answer->answer)
|| '' != trim($answer->min)
&& ((float)$question->answer >= (float)$answer->min)
&& ((float)$question->answer <= (float)$answer->max))
{
//$feedback[0] = $answer->feedback; No feedback was shown for wrong answers
$grade = (float)$answer->fraction;
}
}
$grade *= $question->grade; // Normalize to correct weight
break;
case TRUEFALSE:
if ($question->answer) {
$question->answer = $question->answer[0];
} else {
$question->answer = NULL;
}
foreach($answers as $answer) { // There should be two answers (true and false)
$feedback[$answer->id] = $answer->feedback;
if ($answer->fraction > 0) {
$correct[$answer->id] = true;
}
if ($question->answer == $answer->id) {
$grade = (float)$answer->fraction * $question->grade;
$response[$answer->id] = true;
}
}
break;
case MULTICHOICE:
foreach($answers as $answer) { // There will be multiple answers, perhaps more than one is right
$feedback[$answer->id] = $answer->feedback;
if ($answer->fraction > 0) {
$correct[$answer->id] = true;
}
if (!empty($question->answer)) {
foreach ($question->answer as $questionanswer) {
if ($questionanswer == $answer->id) {
$response[$answer->id] = true;
if ($answer->single) {
$grade = (float)$answer->fraction * $question->grade;
continue;
} else {
$grade += (float)$answer->fraction * $question->grade;
}
}
}
}
}
break;
case MATCH:
$matchcount = $totalcount = 0;
foreach ($question->answer as $questionanswer) { // Each answer is "subquestionid-answerid"
$totalcount++;
$qarr = explode('-', $questionanswer); // Extract subquestion/answer.
$subquestionid = $qarr[0];
$subanswerid = $qarr[1];
if ($subquestionid and $subanswerid and (($subquestionid == $subanswerid) or
($answers[$subquestionid]->answertext == $answers[$subanswerid]->answertext))) {
// Either the ids match exactly, or the answertexts match exactly
// (in case two subquestions had the same answer)
$matchcount++;
$correct[$subquestionid] = true;
} else {
$correct[$subquestionid] = false;
}
$response[$subquestionid] = $subanswerid;
}
$grade = $question->grade * $matchcount / $totalcount;
break;
case RANDOMSAMATCH:
$bestanswer = array();
foreach ($answers as $answer) { // Loop through them all looking for correct answers
if (empty($bestanswer[$answer->question])) {
$bestanswer[$answer->question] = 0;
$correct[$answer->question] = "";
}
if ($answer->fraction > $bestanswer[$answer->question]) {
$bestanswer[$answer->question] = $answer->fraction;
$correct[$answer->question] = $answer->answer;
}
}
$answerfraction = 1.0 / (float) count($question->answer);
foreach ($question->answer as $questionanswer) { // For each random answered question
$rqarr = explode('-', $questionanswer); // Extract question/answer.
$rquestion = $rqarr[0];
$ranswer = $rqarr[1];
$response[$rquestion] = $questionanswer;
if (isset($answers[$ranswer])) { // If the answer exists in the list
$answer = $answers[$ranswer];
$feedback[$rquestion] = $answer->feedback;
if ($answer->question == $rquestion) { // Check that this answer matches the question
$grade += (float)$answer->fraction * $question->grade * $answerfraction;
}
}
}
break;
case MULTIANSWER:
// Default setting that avoids a possible divide by zero:
$subquestion->grade = 1.0;
foreach ($question->answer as $questionanswer) {
// Resetting default values for subresult:
$subresult->grade = 0.0;
$subresult->correct = array();
$subresult->feedback = array();
// Resetting subquestion responses:
$subquestion->answer = array();
$qarr = explode('-', $questionanswer, 2);
$subquestion->answer[] = $qarr[1]; // Always single answer for subquestions
foreach ($answers as $multianswer) {
if ($multianswer->id == $qarr[0]) {
$subquestion->qtype = $multianswer->answertype;
$subquestion->grade = $multianswer->norm;
$subresult = quiz_report_simplestat_grade_attempt_question_result($subquestion, $multianswer->subanswers, true);
break;
}
}
// Summarize subquestion results:
$grade += $subresult->grade;
$feedback[] = $subresult->feedback[0];
$correct[] = $subresult->correct[0];
// Each response instance also contains the partial
// fraction grade for the response:
$response[] = $subresult->grade/$subquestion->grade
. '-' . $subquestion->answer[0];
}
// Normalize grade:
$grade *= $question->grade/($question->defaultgrade);
break;
case DESCRIPTION: // Descriptions are not graded.
break;
case RANDOM: // Returns a recursive call with the real question
$realquestion = get_record
('quiz_questions', 'id', $question->random);
$realquestion->answer = $question->answer;
$realquestion->grade = $question->grade;
return quiz_report_simplestat_grade_attempt_question_result($realquestion, $answers);
}
$result->grade =
$gradecanbenegative ? $grade // Grade can be negative
: max(0.0, $grade); // Grade must not be negative
$result->correct = $correct;
$result->feedback = $feedback;
$result->response = $response;
return $result;
}
function quiz_report_simplestat_remove_unwanted_questions(&$questions, $quiz) {
/// Given an array of questions, and a list of question IDs,
/// this function removes unwanted questions from the array
/// Used by review.php and attempt.php to counter changing quizzes
$quizquestions = array();
$quizids = explode(",", $quiz->questions);
foreach ($quizids as $quizid) {
$quizquestions[$quizid] = true;
}
foreach ($questions as $key => $question) {
if (!isset($quizquestions[$question->id])) {
unset($questions[$key]);
}
}
}
function quiz_report_simplestat_get_answers($question, $answerids=NULL) {
// Given a question, returns the correct answers for a given question
global $CFG;
if (empty($answerids)) {
$answeridconstraint = '';
} else {
$answeridconstraint = " AND a.id IN ($answerids) ";
}
switch ($question->qtype) {
case SHORTANSWER: // Could be multiple answers
return get_records_sql("SELECT a.*, sa.usecase
FROM {$CFG->prefix}quiz_shortanswer sa,
{$CFG->prefix}quiz_answers a
WHERE sa.question = '$question->id'
AND sa.question = a.question "
. $answeridconstraint);
case TRUEFALSE: // Should be always two answers
return get_records("quiz_answers", "question", $question->id);
case MULTICHOICE: // Should be multiple answers
return get_records_sql("SELECT a.*, mc.single
FROM {$CFG->prefix}quiz_multichoice mc,
{$CFG->prefix}quiz_answers a
WHERE mc.question = '$question->id'
AND mc.question = a.question "
. $answeridconstraint);
case MATCH:
return get_records("quiz_match_sub", "question", $question->id);
case RANDOMSAMATCH: // Could be any of many answers, return them all
return get_records_sql("SELECT a.*
FROM {$CFG->prefix}quiz_questions q,
{$CFG->prefix}quiz_answers a
WHERE q.category = '$question->category'
AND q.qtype = ".SHORTANSWER."
AND q.id = a.question ");
case NUMERICAL: // Logical support for multiple answers
return get_records_sql("SELECT a.*, n.min, n.max
FROM {$CFG->prefix}quiz_numerical n,
{$CFG->prefix}quiz_answers a
WHERE a.question = '$question->id'
AND n.answer = a.id "
. $answeridconstraint);
case DESCRIPTION:
return true; // there are no answers for description
case RANDOM:
return quiz_get_answers
(get_record('quiz_questions', 'id', $question->random));
case MULTIANSWER: // Includes subanswers
$answers = array();
$virtualquestion->id = $question->id;
if ($multianswers = get_records('quiz_multianswers', 'question', $question->id)) {
foreach ($multianswers as $multianswer) {
$virtualquestion->qtype = $multianswer->answertype;
// Recursive call for subanswers
$multianswer->subanswers = quiz_get_answers($virtualquestion, $multianswer->answers);
$answers[] = $multianswer;
}
}
return $answers;
default:
return false;
}
}
function quiz_report_simplestat_grade_attempt_results($quiz, $questions) {
/// Given a list of questions (including answers for each one)
/// this function does all the hard work of calculating the
/// grades for each question, as well as a total grade for
/// for the whole quiz. It returns everything in a structure
/// that looks like:
/// $result->sumgrades (sum of all grades for all questions)
/// $result->percentage (Percentage of grades that were correct)
/// $result->grade (final grade result for the whole quiz)
/// $result->grades[] (array of grades, indexed by question id)
/// $result->response[] (array of response arrays, indexed by question id)
/// $result->feedback[] (array of feedback arrays, indexed by question id)
/// $result->correct[] (array of feedback arrays, indexed by question id)
if (!$questions) {
error("No questions!");
}
if (!$grades = get_records_menu("quiz_question_grades", "quiz", $quiz->id, "", "question,grade")) {
error("No grades defined for these quiz questions!");
}
$result->sumgrades = 0;
foreach ($questions as $question) {
$question->grade = $grades[$question->id];
if (!$answers = quiz_report_simplestat_get_answers($question)) {
error("No answers defined for question id $question->id!");
}
$questionresult = quiz_report_simplestat_grade_attempt_question_result($question,
$answers);
// if time limit is enabled and exceeded, return zero grades
if($quiz->timelimit > 0) {
if(($quiz->timelimit + 60) <= $quiz->timesincestart) {
$questionresult->grade = 0;
}
}
$result->grades[$question->id] = round($questionresult->grade, 2);
$result->sumgrades += $questionresult->grade;
$result->feedback[$question->id] = $questionresult->feedback;
$result->response[$question->id] = $questionresult->response;
$result->correct[$question->id] = $questionresult->correct;
}
$fraction = (float)($result->sumgrades / $quiz->sumgrades);
$result->percentage = format_float($fraction * 100.0);
$result->grade = format_float($fraction * $quiz->grade);
$result->sumgrades = round($result->sumgrades, 2);
return $result;
}
class quiz_report extends quiz_default_report {
function display($quiz, $cm, $course) { /// This function just displays the report
global $CFG;
global $download;
optional_variable($download, "");
/// Check to see if groups are being used in this quiz
if ($groupmode = groupmode($course, $cm)) { // Groups are being used
if (empty($download)) { // Display menu too
$currentgroup = setup_and_print_groups($course, $groupmode, "report.php?id=$cm->id&amp;mode=simplestat");
} else {
$currentgroup = get_current_group($course->id);
}
} else {
$currentgroup = false;
}
if ($currentgroup) {
$users = get_group_students($currentgroup, "u.lastname ASC");
} else {
$users = get_course_students($course->id, "u.lastname ASC");
}
$data = array();
$questionorder = explode(',', $quiz->questions);
$count = 0;
foreach ($questionorder as $questionid) {
$count++;
$question[$count] = get_record("quiz_questions", "id", $questionid);
}
/// For each person in the class, get their best attempt
/// and create a table listing results for each person
if ($users) {
foreach ($users as $user) {
$data[$user->id]->firstname = $user->firstname;
$data[$user->id]->lastname = $user->lastname;
$data[$user->id]->grades = array(); // by default
if (!$attempts = quiz_get_user_attempts($quiz->id, $user->id)) {
continue;
}
if (!$bestattempt = quiz_calculate_best_attempt($quiz, $attempts)) {
continue;
}
if (!$questions = quiz_report_simplestat_get_attempt_responses($bestattempt, $quiz)) {
continue;
}
quiz_report_simplestat_remove_unwanted_questions($questions, $quiz);
if (!$results = quiz_report_simplestat_grade_attempt_results($quiz, $questions)) {
error("Could not re-grade this quiz attempt!");
}
$count = 0;
foreach ($questionorder as $questionid) {
$count++;
$data[$user->id]->grades[$count] = $results->grades[$questionid];
}
}
}
$count = count($questionorder);
$total = array();
$average = array();
for ($i=1; $i<=$count; $i++) {
$total[$i] = 0.0;
$average[$i] = 0.0;
}
$datacount = 0;
foreach ($data as $userid => $datum) {
if ($datum->grades) {
$datacount++;
foreach ($datum->grades as $key => $grade) {
$total[$key]+= $grade;
}
}
}
if ($datacount) {
foreach ($total as $key => $sum) {
$average[$key] = format_float($sum/$datacount, 2);
}
}
/// If spreadsheet is wanted, produce one
if ($download == "xls") {
require_once("$CFG->libdir/excel/Worksheet.php");
require_once("$CFG->libdir/excel/Workbook.php");
header("Content-type: application/vnd.ms-excel");
$downloadfilename = clean_filename("$course->shortname ".format_string($quiz->name,true));
header("Content-Disposition: attachment; filename=$downloadfilename.xls");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
header("Pragma: public");
$workbook = new Workbook("-");
// Creating the first worksheet
$myxls = &$workbook->add_worksheet('Simple Quiz Statistics');
/// Print names of all the fields
$myxls->write_string(0,0,format_string($quiz->name,true));
$myxls->set_column(0,0,25);
$myxls->set_column(1,$count,9);
for ($i=1; $i<=$count; $i++) {
$myxls->write_string(0,$i,$i);
}
/// Print all the user data
$row=1;
foreach ($data as $userid => $datum) {
$myxls->write_string($row,0,fullname($datum));
for ($i=1; $i<=$count; $i++) {
if (isset($datum->grades[$i])) {
$myxls->write_number($row,$i,$datum->grades[$i]);
}
}
$row++;
}
/// Print all the averages
for ($i=1; $i<=$count; $i++) {
$myxls->write_number($row,$i,$average[$i]);
}
$formatot =& $workbook->add_format();
// format number 10 is percent, two digit
$formatot->set_num_format(10);
/// Print all the averages as percentages
$row++;
$myxls->write_string($row,0,"%");
for ($i=1; $i<=$count; $i++) {
// $percent = format_float($average[$i] * 100);
// $myxls->write_text($row,$i,"$percent%");
$myxls->write_number($row,$i,$average[$i],$formatot);
}
$workbook->close();
exit;
}
/// If a text file is wanted, produce one
if ($download == "txt") {
/// Print header to force download
header("Content-Type: application/download\n");
$downloadfilename = clean_filename("$course->shortname ".format_string($quiz->name,true));
header("Content-Disposition: attachment; filename=$downloadfilename.txt");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
header("Pragma: public");
/// Print names of all the fields
echo format_string($quiz->name,true);
for ($i=1; $i<=$count; $i++) {
echo "\t$i";
}
echo "\n";
/// Print all the user data
foreach ($data as $userid => $datum) {
echo fullname($datum);
for ($i=1; $i<=$count; $i++) {
echo "\t";
if (isset($datum->grades[$i])) {
echo $datum->grades[$i];
}
}
echo "\n";
}
/// Print all the averages
echo "\t";
for ($i=1; $i<=$count; $i++) {
echo "\t".$average[$i];
}
echo "\n";
/// Print all the averages as percentages
echo "\t%";
for ($i=1; $i<=$count; $i++) {
$percent = format_float($average[$i] * 100);
echo "\t$percent";
}
echo "\n";
exit;
}
/// Otherwise, display the table as HTML
echo "<table border=\"1\" align=\"center\">";
echo "<tr>";
echo "<td>&nbsp;</td>";
for ($i=1; $i<=$count; $i++) {
$title = '';
if (!empty($question[$i]->questiontext)) {
$title = strip_tags($question[$i]->questiontext);
}
echo "<th title=\"$title\">$i</th>";
}
echo "</tr>";
foreach ($data as $userid => $datum) {
echo "<tr>";
echo "<td><b>".fullname($datum)."</b></td>";
if ($datum->grades) {
foreach ($datum->grades as $key => $grade) {
if (isset($grade)) {
echo "<td>$grade</td>";
} else {
echo "<td>&nbsp;</td>";
}
}
}
echo "</tr>";
}
echo "<tr>";
echo "<td>&nbsp;</td>";
for ($i=1; $i<=$count; $i++) {
echo "<td>".$average[$i]."</td>";
}
echo "</tr>";
echo "</table>";
echo "<br />";
echo "<table border=\"0\" align=\"center\"><tr>";
echo "<td>";
unset($options);
$options["id"] = "$cm->id";
$options["mode"] = "simplestat";
$options["noheader"] = "yes";
$options["download"] = "xls";
print_single_button("report.php", $options, get_string("downloadexcel"));
echo "<td>";
$options["download"] = "txt";
print_single_button("report.php", $options, get_string("downloadtext"));
echo "</table>";
return true;
}
}
?>