Skip to content
Snippets Groups Projects
Commit 2ab036ac authored by Matt Butcher's avatar Matt Butcher
Browse files

Reorganization part 2: Old copies of modules are removed.

parent e3b25b27
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 3968 deletions
<?php
// $Id$
/**
* Administration pages for the long answer questions module.
* @file
*/
/**
* Admin settings form.
*/
function long_answer_admin_settings_form() {
$form['long_answer_default_maximum_score'] = array(
'#type' => 'textfield',
'#title' => t('Default Maximum Score'),
// 1 to match default multichoice score.
'#default_value' => variable_get('long_answer_default_maximum_score', 1),
'#description' => t('The default maximum score for a long-answer question.'),
'#required' => TRUE,
);
return system_settings_form($form);
}
/**
* Generate a view of all unscored answer questions.
*
* @see theme_long_answer_view_unscored()
*/
function long_answer_view_unscored() {
$unscored = long_answer_get_all_unscored_answers();
return theme('long_answer_view_unscored', $unscored);
}
/**
* Page handler for displaying a scoring form.
* This function is called directly from the menu router. It generates a form for
* scoring a quiz.
*
* @param $vid
* The VID of the question and answer to load.
* @param $rid
* The result ID of the answer to load.
* @return
* Text to display.
*/
function long_answer_edit_score($vid, $rid) {
// We have to do the vid -> nid lookup ourselves because node_load uses only node.vid,
// and we need to be able to access old nodes in node_revisions.vid. (BTW... why is node singular
// and node_revisions plural?).
$nid = db_result(db_query("SELECT nid FROM {node_revisions} WHERE vid = %d", $vid));
if (!$nid) {
drupal_not_found();
return;
}
$node = node_load($nid, $vid);
if (!$node || $node->type != 'long_answer') {
drupal_not_found();
return;
}
$answer = (object)long_answer_get_answer($node->nid, $node->vid, $rid);
if (!$answer) {
drupal_not_found();
return;
}
drupal_set_title(t('Score answer to "@title"', array('@title' => $node->title)));
return drupal_get_form('long_answer_score_form', $node, $answer);
}
/**
* Build a form for scoring long-answer questions.
*
* @param $node
* The question node.
* @param $answer
* An object containing an answer to the question. This form is for scoring that answer.
* @return
* The form (as a FAPI array).
*/
function long_answer_score_form($context, $node, $answer) {
if(!$node || $node->type != 'long_answer' || !$answer) {
drupal_not_found();
return;
}
// Set up the form
$form['question'] = array(
'#type' => 'item',
'#title' => t('Question'),
'#value' => check_markup($node->body, $node->format),
);
$form['answer'] = array(
'#type' => 'item',
'#title' => t('Answer'),
'#value' => check_markup($answer->answer, variable_get('long_answer_markup_filter', FILTER_FORMAT_DEFAULT)),
);
$form['show_max_score'] = array(
'#type' => 'item',
'#title' => t('Maximum Score'),
'#value' => (int) $node->maximum_score,
);
$form['score'] = array(
'#type' => 'textfield',
'#title' => t('Score'),
'#description' => t('The score for this essay, between 0 and @max', array('@max' => $node->maximum_score)) ,
'#size' => 6,
'#maxlength' => 6,
'#default_value' => (int) $answer->score,
'#required' => TRUE,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save this score'),
);
// Save some work by keeping these.
$form['maximum_score'] = array(
'#type' => 'value',
'#value' => $node->maximum_score,
);
$form['question_nid'] = array(
'#type' => 'value',
'#value' => $answer->question_nid,
);
$form['question_vid'] = array(
'#type' => 'value',
'#value' => $answer->question_vid,
);
$form['result_id'] = array(
'#type' => 'value',
'#value' => $answer->result_id,
);
return $form;
}
function long_answer_score_form_validate($form, $form_state) {
// Check to make sure that entered score is not higher than max allowed score.
$max = (int)$form_state['values']['maximum_score'];
$given = (int)$form_state['values']['score'];
if ($given > $max || $given < 0) {
$args = array('@score' => $given, '@max' => $max);
form_error($form['score'], t('The given score (@score) is higher than the maximum allowed score (@max)', $args));
}
}
function long_answer_score_form_submit($form, &$form_state) {
$sql = 'SELECT nid, vid FROM {quiz_node_results} WHERE result_id = %d';
$result = db_fetch_object(db_query($sql, $form_state['values']['result_id']));
$quiz = node_load($result->nid, $result->vid);
$nid = $form_state['values']['question_nid'];
$vid = $form_state['values']['question_vid'];
$rid = $form_state['values']['result_id'];
$score = $form_state['values']['score'];
$result = long_answer_score_an_answer($quiz, $nid, $vid, $rid, $score);
if($result == 1) {
drupal_set_message("The score has been saved.");
$form_state['redirect'] = 'admin/quiz/score-long-answer';
}
else {
drupal_set_message('Error saving the score. The selected answer was not scored.', 'error');
}
}
/**
* Theme the list of unscored long answer questions.
*
* @param $unscored
* An array of objects, each with the quist_nid, question_vid, and result_id of an unscored question.
*/
function theme_long_answer_view_unscored($unscored) {
$output = '';
$header = array(
t('Quiz ID'),
t('Question'),
t('Time Finished'),
t('Action')
);
$rows = array();
foreach($unscored as $item) {
if ($item->time_end > 0) {
$rows[] = array(
$item->result_id,
$item->title,
date('Y-m-d H:i', $item->time_end),
l('score this response', 'admin/quiz/score-long-answer/' . $item->question_vid . '/' . $item->result_id),
);
}
}
if (!empty($rows)) {
$output .= theme('table', $header, $rows);
}
else {
$output .= t('There are no unscored essays.');
}
return $output;
}
\ No newline at end of file
<?php
// $Id$
/**
* Long answer classes.
*
* @file
*/
/**
* Implementation of QuizQuestion.
*/
class LongAnswerQuestion implements QuizQuestion {
/**
* The current node for this question.
*/
protected $node = NULL;
public function __construct($node) {
$this->node = $node;
}
public function save($is_new = FALSE) {
if (!isset($this->node->feedback)) {
$this->node->feedback = '';
}
if ($is_new || $this->node->revision == 1) {
$sql = 'INSERT INTO {quiz_long_answer_node_properties} (nid, vid, maximum_score) VALUES (%d, %d, %d)';
db_query($sql, $this->node->nid, $this->node->vid, $this->node->maximum_score);
}
else {
$sql = 'UPDATE {quiz_long_answer_node_properties} SET maximum_score = %d WHERE nid = %d AND vid = %d';
db_query($sql, $this->node->maximum_score, $this->node->nid, $this->node->vid);
}
}
public function validate($node, &$form) {
// Check to make sure that the maximum score is greater-than or equal-to 0.
$maximum_score = $node->maximum_score;
if (!ctype_digit($maximum_score) || intval($maximum_score) < 0) {
form_set_error('maximum_score', t('Score must be a positive integer (0 or higher).'));
}
}
public function delete($only_this_version = FALSE) {
if ($only_this_version) {
db_query('DELETE FROM {quiz_long_answer_node_properties} WHERE nid = %d AND vid = %d', $this->node->nid, $this->node->vid);
}
else {
db_query('DELETE FROM {quiz_long_answer_node_properties} WHERE nid = %d', $this->node->nid);
}
}
public function load() {
$sql = 'SELECT maximum_score FROM {quiz_long_answer_node_properties} WHERE nid = %d AND vid = %d';
return db_fetch_object(db_query($sql, $this->node->nid, $this->node->vid));
}
public function view() {
return $this->getQuestionForm($this->node);
}
// This is called whenever a question is rendered, either
// to an administrator or to a quiz taker.
public function getQuestionForm($node, $context = NULL) {
$form['question'] = array(
'#type' => 'markup',
'#value' => $node->body
);
$form['tries'] = array(
'#type' => 'textarea',
'#title' => t('Answer'),
'#description' => t('Enter your answer here. If you need more space, click on the grey bar at the bottom of this area and drag it down.'),
'#rows' => 15,
'#cols' => 60,
'#required' => FALSE,
);
return $form;
}
public function getAdminForm($edit = NULL) {
$form['settings'] = array(
'#type' => 'markup',
'#value' => t('There are no settings for this question type.'),
);
return $form;
}
public function getCreationForm($edit) {
$form['maximum_score'] = array(
'#type' => 'textfield',
'#title' => t('Maximum Possible Score'),
'#description' => t('Long answer questions are scored manually. This field indicates to the person scoring what the maximum number of points per essay is. Multichoice questions have a score of 1.'),
'#default_value' => isset($this->node->maximum_score) ? $this->node->maximum_score : variable_get('long_answer_default_maximum_score', 1),
'#size' => 3,
'#maxlength' => 3,
'#required' => TRUE,
);
return $form;
}
public function getMaximumScore() {
return $this->node->maximum_score;
}
}
class LongAnswerResponse extends AbstractQuizQuestionResponse {
/**
* Get all quiz scores that have not yet been evaluated.
*
* @param $count
* Number of items to return (default: 50).
* @param $offset
* Where in the results we should start (default: 0).
*
* @return
* Array of objects describing unanswered questions. Each object will have result_id, question_nid, and question_vid.
*/
public static function fetchAllUnscoredAnswers($count = 50, $offset = 0) {
$sql = 'SELECT a.result_id, a.question_nid, a.question_vid, r.title, n.time_end, n.time_start, n.uid
FROM {quiz_long_answer_user_answers} AS a
INNER JOIN {node_revisions} AS r ON a.question_vid = r.vid
INNER JOIN {quiz_node_results} AS n ON a.result_id = n.result_id
WHERE is_evaluated = 0';
$results = db_query_range($sql, $offset, $count);
$unscored = array();
if ($results) {
while ($row = db_fetch_object($results)) {
$unscored[] = $row;
}
}
return $unscored;
}
/**
* Given a quiz, return a list of all of the unscored answers.
*
* @param $nid
* Node ID for the quiz to check.
* @param $vid
* Version ID for the quiz to check.
* @param $count
* Number of items to return (default: 50).
* @param $offset
* Where in the results we should start (default: 0).
*
* @return
* Indexed array of result IDs that need to be scored.
*/
public static function fetchUnscoredAnswersByQuestion($nid, $vid, $count = 50, $offset = 0) {
$results = db_query_range('SELECT result_id FROM {quiz_long_answer_user_answers} WHERE is_evaluated = 0 AND question_nid = %d AND question_vid = %d', $nid, $vid, $offset, $count);
$unscored = array();
foreach (db_fetch_object($results) as $row) {
$unscored[] = $row->result_id;
}
return $unscored;
}
/**
* ID of the answer.
*/
protected $answer_id = 0;
public function __construct($rid, $question, $answer = NULL) {
$this->rid = $rid;
$this->question = $question;
if (!isset($answer)) {
$sql = "SELECT answer_id, answer, is_evaluated, score, question_vid, question_nid, result_id
FROM {quiz_long_answer_user_answers}
WHERE question_nid = %d AND question_vid = %d AND result_id = %d";
$r = db_fetch_object(db_query($sql, $question->nid, $question->vid, $rid));
if (!empty($r)) {
$this->answer = $r->answer;
$this->score = $r->score;
$this->evaluated = $r->is_evaluated;
$this->answer_id = $r->answer_id;
}
}
else {
$this->answer = $answer;
}
}
public function save() {
$sql = "INSERT INTO {quiz_long_answer_user_answers} (answer, question_nid, question_vid, result_id) VALUES ('%s', %d, %d, %d)";
db_query($sql, $this->answer, $this->question->nid, $this->question->vid, $this->rid);
$this->answer_id = db_last_insert_id('quiz_long_answer_user_answers', 'answer_id');
}
public function delete() {
$sql = 'DELETE FROM {quiz_long_answer_user_answers} WHERE question_nid = %d AND question_vid = %d AND result_id = %d';
db_query($sql, $this->question->nid, $this->question->vid, $this->rid);
}
public function score() {
$sql = "SELECT score FROM {quiz_long_answer_user_answers} WHERE result_id = %d AND question_vid = %d";
$score = (int)db_result(db_query($sql, $this->rid, $this->question->vid));
$this->score = $score;
return $score;
}
public function isCorrect() {
$possible = _quiz_question_get_instance($this->question)->getMaximumScore();
$actual = $this->score;
return (($actual / $possible) * 100 > 50);
}
public function getResponse() {
return $this->answer;
}
public function formatReport($showpoints = TRUE, $showfeedback = TRUE) {
$slug = '<div class="quiz_summary_question"><span class="quiz_question_bullet">Q:</span> '.
check_markup($this->question->body) .
'</div>';
$result = '<div class="quiz_answer_feedback">';
if ($this->question && !empty($this->question->answers)) {
$answer = (object) current($this->question->answers);
if ($answer->is_evaluated == 1) {
// Show score:
if ($showpoints) {
$args = array('@yours' => $answer->score, '@total' => $this->question->maximum_score);
$result .= t('Score: @yours of @total possible points', $args);
}
// Show feedback, if any.
if ($showfeedback && !empty($answer->feedback)) {
$result .= '</div><div class="quiz_answer_feedback">' . $answer->feedback;
}
}
else {
$result .= t('This answer has not yet been scored.') .
'<br/>' .
t('Until the answer is scored, the total score will not be correct.');
}
if (user_access('score long answer')) {
$path = sprintf('admin/quiz/score-long-answer/%s/%s', $this->question->vid, $this->rid);
$result .= '<p>' . l(t('Score this answer'), $path) . '</p>';
}
}
else {
$result .= t('This question was not answered.');
}
$result .= '</div>';
return $slug . $result;
}
}
; $Id$
name = Long Answer
package = Quiz
description = This provides long answer (essay, multi-paragraph) question types for use by the Quiz module.
dependencies[] = quiz
dependencies[] = quiz_question
core = 6.x
php = 5
\ No newline at end of file
<?php
// $Id$
/**
* Long answer questions.
* @file
*/
/**
* Implementation of hook_install().
*/
function long_answer_install() {
drupal_install_schema('long_answer');
}
/**
* Implementation of hook_uninstall().
*/
function long_answer_uninstall() {
// Delete tables
drupal_uninstall_schema('long_answer');
// Delete data from other tables
// Clear the cache.
cache_clear_all('variables', 'cache');
drupal_set_message(t('The Long Answer module has been uninstalled and related data has been deleted.'));
}
/**
* Implementation of hook_schema().
*/
function long_answer_schema() {
// Properties for a question nodes go in here:
$schema['quiz_long_answer_node_properties'] = array(
'fields' => array(
'nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'maximum_score' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
),
),
'primary key' => array('nid', 'vid'),
);
// User answers go in here.
$schema['quiz_long_answer_user_answers'] = array(
'fields' => array(
'answer_id' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'question_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'question_vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'result_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'score' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'is_evaluated' => array(
'type' => 'int',
'unsigned' => TRUE,
'size' => 'tiny',
'default' => 0,
'not null' => TRUE,
),
'answer' => array(
'type' => 'text'
),
),
'primary key' => array('answer_id'),
'unique keys' => array(
'ids' => array('result_id', 'question_nid', 'question_vid'),
),
);
return $schema;
}
\ No newline at end of file
<?php
// $Id$
/**
* This module defines a long answer question type for quizzes.
*
* Long answer questions make use of the quiz question framework (quiz_question.module).
* The functions in this file are largely used for grading long answer questions. Most
* of the real logic is in long_answer.classes.inc.
*
* @file
*/
/**
* Implementation of hook_help().
*/
function long_answer_help($path, $arg) {
if ($path == 'admin/help#long_answer') {
return '<p>' . t('This module provides long-answer (essay, multi-paragraph) questions to the quiz module.') .
'</p><p>' . t('A long-answer question is designed to provide the quiz taker a lengthy area to expand on ideas.
Common forms of long-answer questions include essays, single paragraph responses, hypothesis design problems,
outlines and summaries, and lengthier math problems
where the focus is on showing work rather than simply getting the correct answer.') . '</p>';
}
}
/**
* Implementation of hook_menu().
*/
function long_answer_menu() {
/*
$items['admin/quiz/long_answer'] = array(
'title' => t('Long-answer configuration'),
'description' => t('Configure long-answer questions for users.'),
'page callback' => 'drupal_get_form',
'page arguments' => array('long_answer_admin_settings_form'),
'access arguments' => array(QUIZ_PERM_ADMIN_CONFIG),
'type' => MENU_NORMAL_ITEM,
'file' => 'long_answer.admin.inc',
);
*/
$items['admin/quiz/score-long-answer'] = array(
'title' => t('Score long-answer questions'),
'description' => t('Score the answers from quizzes that use long answer questions.'),
'page callback' => 'long_answer_view_unscored',
//'page arguments' => array('long_answer_admin_settings_form'),
'access arguments' => array('score long answer'),
'type' => MENU_NORMAL_ITEM,
'file' => 'long_answer.admin.inc',
);
// Pass vid and rid to this path.
$items['admin/quiz/score-long-answer/%/%'] = array(
'title' => t('Score long-answer response'),
'description' => t('Score a response to a long-answer question.'),
'page callback' => 'long_answer_edit_score',
'page arguments' => array(3, 4),
'type' => MENU_NORMAL_ITEM,
'access arguments' => array('score long answer'),
'file' => 'long_answer.admin.inc'
);
return $items;
}
/**
* Implementation of hook_quiz_question_info().
*/
function long_answer_quiz_question_info() {
return array(
'long_answer' => array(
'name' => 'Long answer question',
'description' => 'Quiz questions that allow a user to enter multiple paragraphs of text.',
'question provider' => 'LongAnswerQuestion',
'response provider' => 'LongAnswerResponse',
'module' => 'quiz_question', // All wrapper functions are in that module.
),
);
}
/**
* Implementation of hook_autoload_info().
*/
function long_answer_autoload_info() {
return array(
'LongAnswerQuestion' => array('file' => 'long_answer.classes.inc'),
'LongAnswerResponse' => array('file' => 'long_answer.classes.inc'),
);
}
/**
* Implementation of hook_theme().
*/
function long_answer_theme() {
return array(
'long_answer_report' => array(
'arguments' => array('question' => NULL, 'show_points' => NULL, 'show_feedback' => NULL),
'file' => 'long_answer.theme.inc',
),
'long_answer_feedback' => array(
'arguments' => array('quiz' => NULL, 'report' => NULL),
'file' => 'long_answer.theme.inc',
),
'long_answer_view_unscored' => array(
'arguments' => array('unscored' => array()),
'file' => 'long_answer.admin.inc',
),
);
}
/**
* Set a score for a long answer question.
*
* This stores a score for a long answer question and marks that question as having been evaluated.
* The function updates all of the necessary data sources so that the individual answer results should be
* reflected in the total scoring table.
*
* @param $quiz
* Quiz node.
* @param $nid
* Node ID of question.
* @param $vid
* Version ID of question.
* @param $rid
* Result ID for the quiz results.
* @param $score
* The numeric score to assign the result.
*
* @return int
* Number of scores adjusted. If a change was made, this should be 1.
*/
function long_answer_score_an_answer($quiz, $nid, $vid, $rid, $score) {
// Quiz scoring information is spread out across three tables:
// 1. The module should retain its own scoring information in any case where scoring is non-trivial.
// 2. The Quiz module (needlessly?) retains a limited amount of scoring information. This appears to be used only
// infrequently.
// 3. The Quiz module retains an overall score for a quiz. This is the percentage score for the combination of all
// questions on the quiz.
//
// We update all three.
// First, we update the long answer table
db_query("UPDATE {quiz_long_answer_user_answers} SET score = %d, is_evaluated = 1 WHERE question_nid = %d AND question_vid = %d AND result_id = %d", $score, $nid, $vid, $rid);
$changed = db_affected_rows();
if ($changed > 0) {
// Second, we update the main quiz answers table
// What do we do about the quiz_node_results_answers table? It assumes strict
// bivalence (is_correct). I guess we consider any essay with over 50% to be correct?
$max = db_result(db_query('SELECT maximum_score FROM {quiz_long_answer_node_properties} WHERE vid = %d', $vid));
if ($max <= 0) {
$is_correct = 0;
$points_awarded = 0;
}
else {
$is_correct = (($score * 100) / $max > 50) ? 1 : 0;
$points_awarded = $score;
}
$sql = 'UPDATE {quiz_node_results_answers}
SET points_awarded = %d, is_correct = %d
WHERE question_vid = %d AND result_id = %d';
db_query($sql, $points_awarded, $is_correct, $vid, $rid);
// Third, we update the main quiz results table
quiz_update_total_score($quiz, $rid);
}
return $changed;
}
/**
* Set the answer for a question.
*
* This stores a score for a long answer question and marks that question as having been evaluated.
* @param $nid
* Node ID of question.
* @param $vid
* Version ID of question.
* @param $rid
* Result ID for the quiz results.
*
* @return Assoc array
* An array if successful, or FALSE if no result could be found. The array contains the following properties:
* <code>
* answer_id; // The answer ID
* answer; // The full text of the answer
* is_evaluated; // 0 if the question has not been evaluated, 1 if it has
* score; // The score the evaluator gave the user; this should be 0 if is_evaluated is 0.
* question_vid
* question_nid
* result_id
* </code>
*/
function long_answer_get_answer($question_nid, $question_vid, $result_id) {
$sql = "SELECT answer_id, answer, is_evaluated, score, question_vid, question_nid, result_id
FROM {quiz_long_answer_user_answers}
WHERE question_nid = %d AND question_vid = %d AND result_id = %d";
$results = db_query($sql, $question_nid, $question_vid, $result_id);
if (!$results) {
return FALSE;
}
return db_fetch_array($results);
}
/**
* Given a quiz, return a list of all of the unscored answers.
*
* @param $nid
* Node ID for the quiz to check.
* @param $vid
* Version ID for the quiz to check.
* @param $count
* Number of items to return (default: 50).
* @param $offset
* Where in the results we should start (default: 0).
*
* @return
* Indexed array of result IDs that need to be scored.
*/
function long_answer_get_unscored_answers_by_question($nid, $vid, $count = 50, $offset = 0) {
$results = db_query_range('SELECT result_id FROM {quiz_long_answer_user_answers} WHERE is_evaluated = 0 AND question_nid = %d AND question_vid = %d', $nid, $vid, $offset, $count);
$unscored = array();
foreach (db_fetch_object($results) as $row) {
$unscored[] = $row->result_id;
}
return $unscored;
}
/**
* Get all quiz scores that have not yet been evaluated.
*
* @param $count
* Number of items to return (default: 50).
* @param $offset
* Where in the results we should start (default: 0).
*
* @return
* Array of objects describing unanswered questions. Each object will have result_id, question_nid, and question_vid.
*/
function long_answer_get_all_unscored_answers($count = 50, $offset = 0) {
$sql = 'SELECT a.result_id, a.question_nid, a.question_vid, r.title, n.time_end, n.time_start, n.uid
FROM {quiz_long_answer_user_answers} AS a
INNER JOIN {node_revisions} AS r ON a.question_vid = r.vid
INNER JOIN {quiz_node_results} AS n ON a.result_id = n.result_id
WHERE is_evaluated = 0';
$results = db_query_range($sql, $offset, $count);
$unscored = array();
if ($results) {
while ($row = db_fetch_object($results)) {
$unscored[] = $row;
}
}
return $unscored;
}
\ No newline at end of file
<?php
// $Id$
/**
* Unit tests for the long_answer Module.
* @file
*/
class LongAnswerUnitTest extends DrupalWebTestCase {
var $question_node_type = 'long_answer';
var $nid1 = NULL;
var $max_score = 1234; // Impossible value, given max limit of 3 digits.
var $title = '';
var $body = '';
public function getInfo() {
return array(
'name' => t('Long answer unit tests.'),
'description' => t('Suite of unit tests for verifying functionality for essay question functions.'),
'group' => t('Quiz'),
);
}
public function setUp() {
parent::setUp('quiz', 'views', 'long_answer');
}
//public function testModuleIsEnabled() {
// $this->assertTrue(in_array('long_answer', module_list(FALSE, TRUE, TRUE)), t('Test that module is installed.'));
//}
/**
* Check that hook_quiz_question_info is working.
*/
public function testQuizQuestionInfo() {
$info = long_answer_quiz_question_info();
$this->assertEqual(count($info), 1, t('Check that info was returned.'));
$this->assertTrue(isset($info['long_answer']), t('Check that long answer question type exists.'));
}
/**
* Utility function for creating a new test question.
*/
public function createLongAnswerQuestion() {
$this->title = 'Long Answer Question';
$this->body = 'This is a question?';
$this->max_score = 999;
$settings = array(
'type' => $this->question_node_type,
'title' => $this->title,
'body' => $this->body,
'maximum_score' => $this->max_score,
'revisions' => TRUE,
);
$node = $this->drupalCreateNode($settings);
return $node;
}
/*
public function createQuiz() {
$title = 'Test Quiz';
$body = 'This is a sample quiz.';
$settings = array(
'type' => 'quiz',
'title' => $title,
'body' => $body,
'revisions' => TRUE,
);
$node = $this->drupalCreateNode($settings);
return $node;
}
*/
/**
* Run a bundle of Node API tests.
*
* This tests CRUD and revision functionality on a single node.
*/
public function testNodeAPI() {
$this->unitTestCreateQuestionNode();
$this->unitTestCheckNodeProperties();
$this->unitTestUpdateQuestionNode();
$this->unitTestListQuestions();
// Revisions
$this->unitTestCreateQuestionRevision();
$this->unitTestDeleteQuestionNode();
}
/**
* Create and then update a node.
*/
public function unitTestCreateQuestionNode() {
$node = $this->createLongAnswerQuestion();
if (!$node) {
throw new Exception('Expected to have a node to work with.');
}
$this->nid1 = $node->nid;
$this->assertEqual($node->title, $this->title, t('Title of stored node should equal the original title.'));
$this->assertEqual($node->body, $this->body, t('Body of stored node should be equal to original body.'));
$this->assertEqual($node->maximum_score, $this->max_score, t('Stored score should be the same as original score.'));
$this->assertEqual($node->type, $this->question_node_type, t('Stored node type should be long_answer'));
}
/**
* Test that the appropriate data was stored in the node properties table.
*/
public function unitTestCheckNodeProperties() {
$max = db_result(db_query('SELECT maximum_score FROM {quiz_long_answer_node_properties} WHERE nid = %d', $this->nid1));
$this->assertEqual($max, $this->max_score, t('Test that max score was appropriately stored in database.'));
}
/**
* Check that question exists in DB.
*/
public function unitTestListQuestions() {
$questions = long_answer_list_questions();
drupal_set_message("Question Count: " . count($questions));
$this->assertEqual(count($questions), 1, t('Verify that the question exists.'));
}
/**
* Test updating of a question.
*/
public function unitTestUpdateQuestionNode() {
$newScore = 2;
$node = node_load($this->nid1);
$node->maximum_score = $newScore;
node_save($node);
$nodeCopy = node_load($node->nid, $node->vid);
$this->assertEqual($nodeCopy->maximum_score, $newScore, t('Check that stored score is the same as newly assigned score.'));
}
/**
* Test creation of node revision.
*/
public function unitTestCreateQuestionRevision() {
$node = node_load($this->nid1);
$oldVid = $node->vid;
$node->revision = 1;
node_save($node);
$oldMax = $node->maximum_score;
$nodeCopy = node_load($node->nid, $node->vid);
$this->assertNotEqual($nodeCopy->vid, $oldVid, t('Check that VID of new version is not the same as old VID.'));
$this->assertEqual($oldMax, $nodeCopy->maximum_score, t('Check that new VID has an entry in node properties.'));
}
/**
* Test deeting of question node.
*/
public function unitTestDeleteQuestionNode() {
$node = node_load($this->nid1);
if ($node === FALSE) {
throw new Exception("Expected fixture to have valid nid1");
}
unset($node);
node_delete($this->nid1);
$node = node_load($this->nid);
$this->assertFalse($node, t('Test that node was deleted'));
}
/**
* The main callback for answering a question.
*/
public function testEvaluateQuestion() {
$result = long_answer_evaluate_question($question, 1);
$this->assertTrue((count($result) > 0), t('See if a result was returned from the evaluator.'));
$this->assertEqual($result->is_correct, 0, t('Test that answer was not marked correct.'));
}
/**
* Test suite to write, update and delete answers.
*/
public function testSaveDeleteScoreAnswer() {
$this->unitTestSaveAnswer();
$this->unitTestGetUnscoredAnswers();
$this->unitTestScoreAnswer();
$this->unitTestGetAnswer();
$this->unitTestDeleteAnswer();
}
/**
* Test saving an answer.
*/
public function unitTestSaveAnswer() {
$answer_id = long_answer_save_answer(1, 1, 1, 'THIS IS A TEST ANSWER');
// FIXME: This should just select * from the table and check to make sure there is an entry.
// We should not assume IDs are serial.
$this->assertEqual($answer_id, 1, 'Answer ID is set to 1 -- first answer');
}
/**
* Test modifying (scoring) an answer.
*/
public function unitTestScoreAnswer() {
$change = long_answer_score_an_answer(1, 1, 1, 30);
$this->assertEqual($change, 1, t('Only one row was changed.'));
$unanswered = long_answer_get_all_unscored_answers();
$this->assertEqual(count($unanswered), 0, t('There should be no unscored questions.'));
}
public function unitTestGetAnswer() {
$answer = long_answer_get_answer(1, 1, 1);
$this->assertTrue(($answer !== FALSE), t('Check that an answer was returned.'));
$this->assertEqual($answer['is_evaluated'], 1, t('Check that answer is scored.'));
$this->assertEqual($answer['answer_id'], 1, t('Check that this is first answer'));
$this->assertEqual($answer['answer'], 'THIS IS A TEST ANSWER', t('Check that answer text is expected.'));
}
/**
* Test deleting an answer.
*/
public function unitTestDeleteAnswer() {
$removed = long_answer_delete_answer(1, 1, 1);
$this->assertEqual($removed, 1, t('A single answer should be deleted.'));
}
public function unitTestGetUnscoredAnswers() {
$unanswered = long_answer_get_all_unscored_answers();
drupal_set_message(__FUNCTION__ . ' Unanswered: ' . count($unanswered));
$this->assertEqual(count($unanswered), 1, t('There should be only one unanswered question in *all* questions.'));
$unanswered = long_answer_get_unscored_answers_by_question(1,1);
$this->assertEqual(count($unanswered), 1, t('There should be only one unanswered question for the quiz.'));
}
}
/*
// Functional tests are a pain in the butt when it comes to creating new nodes.
class LongAnswerFunctionalTest extends DrupalWebTestCase {
public function getInfo() {
return array(
'name' => t('Long answer quiz question functional tests.'),
'description' => t('Suite of functional tests for verifying functionality for essay questions.'),
'group' => t('Quiz'),
);
}
public function setUp() {
parent::setUp('quiz', 'views', 'long_answer');
}
public function testEditScore() {
$node = $this->createLongAnswerQuestion();
$rid = 1;
$answer_text = 'This is a test answer';
$answer_id = long_answer_save_answer($node->nid, $node->vid, $rid, $answer_text);
$form = long_answer_edit_score($node->vid, $rid);
print_r($form);
$pattern = '|' . $answer_text . '|';
$this->assertText($form['answer']['#value'], check_markup($answer->answer), t('Test that answer was correctly loaded'));
}
public function testCreateQuestion() {
$path = 'node/add/long-answer';
$submit = 'Save';
$data = array(
);
}
public function testModifyQuestion() {
}
public function testAnswerQuestion() {
}
public function testScoringQuestion() {
}
}
*/
\ No newline at end of file
<?php
// $Id$
/**
* @file
* Theme functions for long_answer.
*/
/**
* Implementation of theme_$type_report().
* Theme the feedback report for a quiz. This report is generated (typically) at the end of the quiz.
*/
function theme_long_answer_report($question, $show_points, $show_feedback) {
if (is_array($question)) {
// Cast this if it is an assoc array.
$question = (object)$question;
}
$slug = '<div class="quiz_summary_question"><span class="quiz_question_bullet">Q:</span> '.
check_markup($question->body) .
'</div>';
$result = '<div class="quiz_answer_feedback">';
if ($question && !empty($question->answers)) {
$answer = (object) current($question->answers);
if ($answer->is_evaluated == 1) {
// Show score:
if ($show_points) {
$args = array('@yours' => $answer->score, '@total' => $question->maximum_score);
$result .= t('Score: @yours of @total possible points', $args);
}
// Show feedback, if any.
if ($show_feedback && !empty($answer->feedback)) {
$result .= '</div><div class="quiz_answer_feedback">' . $answer->feedback;
}
}
else {
$result .= t('This answer has not yet been scored.') .
'<br/>' .
t('Until the answer is scored, the total score will not be correct.');
}
if (user_access('score long answer')) {
$path = sprintf('admin/quiz/score-long-answer/%s/%s', $question->vid, $answer->result_id);
$result .= '<p>' . l(t('Score this answer'), $path) . '</p>';
}
}
else {
$result .= t('This question was not answered.');
}
$result .= '</div>';
return $slug . $result;
}
/**
* Implementation of theme_$type_feedback().
* This is basically useless for long answer questions.
*/
function theme_long_answer_feedback($quiz, $report) {
$output = '<div class="quiz_summary_text"><strong>' . t('Q:') . '</strong>'.
check_markup($report->body, $report->format) .
'<br /><em>'.
t('The answer to this question will be scored by hand.')
. '</em></div>';
return $output;
}
\ No newline at end of file
; $Id$
name = Multichoice
package = Quiz
description = This provides multiple choice questions for use by the Quiz module.
dependencies[] = quiz
core = 6.x
\ No newline at end of file
<?php
// $Id$
/**
* @file
* Multichoice Install (a quiz question type)
*/
/**
* Implementation of hook_update_N().
* Change the question field to a text field to allow long questions.
*/
function multichoice_update_6300() {
$ret = array();
$spec = array('type' => 'text');
db_change_field($ret, 'quiz_multichoice_answers', 'answer', 'answer', $spec);
return $ret;
}
/**
* Implementation of hook_install()
*/
function multichoice_install() {
drupal_install_schema('multichoice');
}
/**
* Implementation of hook_schema().
*/
function multichoice_schema() {
/**
* Stores correct answers for multichoice quiz.
*/
// Create the quiz node user answers multichoice table.
$schema['quiz_multichoice_user_answers'] = array(
'fields' => array(
'question_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'question_vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'result_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'answer_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
),
'primary key' => array(
'result_id', 'question_nid', 'question_vid', 'answer_id',
),
);
/**
* Stores user answers for multichoice quiz.
*/
// Create the quiz node answers multichoice table.
$schema['quiz_multichoice_answers'] = array(
'fields' => array(
'answer_id' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
//'answer' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE),
'answer' => array(
'type' => 'text'
),
'feedback' => array(
'type' => 'text',
),
'result_option' => array(
'type' => 'int',
'unsigned' => TRUE,
'default' => 0,
),
'is_correct' => array(
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'default' => 0,
),
),
'primary key' => array(
'answer_id',
),
);
// Default the "Show Author and Date" for this question type to OFF.
$temp_array = variable_get('theme_settings','' /*$default*/);
$temp_array['toggle_node_info_multichoice'] = 0;
variable_set('theme_settings', $temp_array);
return $schema;
}
/**
* Implementation of hook_uninstall()
*/
function multichoice_uninstall() {
drupal_uninstall_schema('multichoice');
// Delete from nodes and node_revisions.
/*
switch ($GLOBALS['db_type']) {
case 'mysql':
case 'mysqli':
db_query('DELETE FROM {node}, {node_revisions}, {quiz_node_question_properties} USING {node} LEFT JOIN {node_revisions} USING (nid) LEFT JOIN {quiz_node_question_properties} USING (nid) WHERE type IN ("multichoice")');
break;
case 'pgsql':
db_query("DELETE FROM {quiz_node_question_properties} WHERE nid IN (SELECT nid FROM {node} WHERE type IN ('multichoice'))");
db_query("DELETE FROM {node_revisions} WHERE nid IN (SELECT nid FROM {node} WHERE type IN ('multichoice'))");
db_query("DELETE FROM {node} WHERE type IN ('multichoice')");
break;
}
*/
// Truncate the cache so users don't run into any unexpected errors.
cache_clear_all('variables', 'cache');
// Inform the user that uninstall was sucessful.
drupal_set_message(t("The Multichoice module has been uninstalled. Multichoice nodes may still exist, but they will not function properly."));
}
\ No newline at end of file
This diff is collapsed.
File moved
<?php
/**
* quiz_directions.classes
*
* This module uses the question interface to define something which is actually not a question.
*
* A Quiz Directions node is a placeholder for adding directions to a quiz. It can be inserted any number
* of times into a quiz. Example uses may include:
*
* - Initial quiz-wide directions
* - Section directions, e.g. "The next five questions are multiple choice, please..."
* - Final confirmation, e.g. "You have answered all questions. Click submit to submit this quiz."
*
* @file
*/
/**
* Implementation of QuizQuestion.
*/
class QuizDirectionsQuestion implements QuizQuestion {
/**
* The current node for this question.
*/
protected $node = NULL;
public function __construct($node) {
$this->node = $node;
$this->node->no_skip_button = TRUE; // No skip button
$this->node->not_a_question = TRUE;
}
public function save($is_new = FALSE) {}
public function validate($node, &$form) {}
public function delete($only_this_version = FALSE) {}
public function load() {}
public function view() {
return $this->getQuestionForm($this->node);
}
// This is called whenever a question is rendered, either
// to an administrator or to a quiz taker.
public function getQuestionForm($node, $context = NULL) {
$form['question'] = array(
'#type' => 'markup',
'#value' => $node->body
);
$form['tries'] = array(
'#type' => 'hidden',
'#value' => 0,
);
$form['empty_space'] = array(
'#type' => 'markup',
'#value' => '<br/>',
);
return $form;
}
public function getAdminForm($edit = NULL) {
$form['settings'] = array(
'#type' => 'markup',
'#value' => t('There are no settings for this question type.'),
);
return $form;
}
public function getCreationForm($edit) {
return array();
}
public function getMaximumScore() {
return 0;
}
}
/**
* Class that describes a "Directions question response". For
* the most part, no real scoring takes place for a direction
* node. However, there are a few behind-the-scenes tricks that
* are done here to make the quiz-taking process a little easier.
*/
class QuizDirectionsResponse extends AbstractQuizQuestionResponse {
public function __construct($rid, $question, $answer = NULL) {
$this->answer = $answer;
$this->question = $question;
$this->rid = $rid;
}
public function save() {}
public function delete() {}
public function score() {
// First, due to popular demand, if the directions are at the beginning of
// the quiz, we restart the timer after the user has read the question.
$quiz_nid = arg(1);
$quiz_key = 'quiz_' . $quiz_nid;
if (isset($_SESSION[$quiz_key]['previous_quiz_questions']) && count($_SESSION[$quiz_key]['previous_quiz_questions']) === 1) {
// reset the timer.
$sql = 'UPDATE {quiz_node_results} SET time_start=%d WHERE result_id = %d';
db_query($sql, time(), $this->rid);
}
// Set the score
$this->score = 0;
return 0;
}
public function isCorrect() {
return TRUE;
}
public function getResponse() {
return $answer;
}
public function formatReport($showpoints = TRUE, $showfeedback = TRUE) {
return '';
}
}
;$Id$
name = Quiz Direction
package = Quiz
description = Provide a slot to add directions for a quiz or some sub-portion of a quiz.
core = 6.x
php = 5
dependencies[] = quiz
dependencies[] = quiz_question
\ No newline at end of file
<?php
// $Id$
/**
* The main file for quiz_directions.
*
* Directions are implemented as a question type. However, they do not affect the score.
* @file
*/
/**
* Implementation of hook_help().
*/
function quiz_directions_help($path, $args) {
if ($path == 'admin/help#quiz_directions') {
return t('Provides directions which can be inserted alongside questions in a quiz.');
}
}
/**
* Implementation of hook_quiz_question_info().
*/
function quiz_directions_quiz_question_info() {
return array(
'quiz_directions' => array(
'name' => 'Quiz directions',
'description' => 'Quiz directions allow you to drop in directions anywhere in a quiz. You can also use them to insert a confirmation step at the end of the quiz.',
'question provider' => 'QuizDirectionsQuestion',
'response provider' => 'QuizDirectionsResponse',
'module' => 'quiz_question', // All wrapper functions are in that module.
),
);
}
/**
* Implementation of hook_autoload_info().
*/
function quiz_directions_autoload_info() {
return array(
'QuizDirectionsQuestion' => array('file' => 'quiz_directions.classes.inc'),
'QuizDirectionsResponse' => array('file' => 'quiz_directions.classes.inc'),
);
}
\ No newline at end of file
<?php
/**
* Classes used in the Quiz Question module.
*
* The core of the Quiz Question module is a set of interfaces and abstract
* classes that can be used to quickly and efficiently create new question
* types.
*
* Why OO?
* Drupal has a long history of avoiding many of the traditional OO structures
* and metaphors. However, with PHP 5, there are many good reasons to use OO
* principles more broadly.
*
* The case for Quiz question types is that question types all share common
* structure and logic. Using the standard hook-only Drupal metaphor, we are
* forced to copy and past large amounts of repetitive code from question
* type to question type. By using OO principles and construction, we can
* easily encapsulate much of that logic, while still making it easy to
* extend the existing content.
*
* Where do I start?
* To create a new question type, check out the True/False type that is included
* in this module.
*
* @file
*/
/**
* A Quiz Question.
* There should be an implementation of this interface for every question type support
* by Quiz.
*/
interface QuizQuestion {
/**
* Construct a new quiz question.
*/
public function __construct($node);
/**
* Responsible for handling insert/update of question-specific data.
* This is typically called from within the Node API, so there is no need
* to save the node. This function is only responsible for saving data
* specific to the implement ation.
*
* The $is_new flag is set to TRUE whenever the node is being initially
* created.
*
* A save function is required to handle the following three situations:
* - A new node is created ($is_new is TRUE)
* - A new node *revision* is created ($is_new is NOT set, because the
* node itself is not new).
* - An existing node revision is modified.
*
* @param $is_new
* TRUE when the node is initially created.
*/
public function save($is_new = FALSE);
/**
* Provides validation for question before it is created.
*
* When a new question is created and initially submited, this is
* called to validate that the settings are acceptible.
*
* @param $node
* The node storing the question.
* @param $form
* The processed form.
*/
public function validate($node, &$form);
/**
* Deletes a question from the database.
*
* @param $only_this_version
* If the $only_this_version flag is TRUE, then only the particular
* nid/vid combo should be deleted. Otherwise, all questions with the
* current nid can be deleted.
*/
public function delete($only_this_version = FALSE);
/**
* Retrieve information relevant for viewing the node.
* This data is generally added to the node's extra field.
*/
public function view();
/**
* Retrieve information about the question and add it to the node.
*/
public function load();
/**
* Get the form that will be displayed to the test-taking user.
*
* @param $node
* The question node.
* @param $context
* The form context.
* @return
* Must return a FAPI array.
*/
public function getQuestionForm($node, $context = NULL);
/**
* Get the form that contains admin settings for this question type.
*
* @return
* Must return a FAPI array.
*/
public function getAdminForm($edit = NULL);
/**
* Get the form used to create a new question.
* @return
* Must return a FAPI array.
*/
public function getCreationForm($edit);
/**
* Get the maximum possible score for this question.
*/
public function getMaximumScore();
}
/**
* Each question type must store its own response data and be able to calculate a score for
* that data.
*/
interface QuizQuestionResponse {
/**
* Create a new user response.
*
* @param $rid
* The result ID for the user's result set. There is one result ID per time
* the user takes a quiz.
* @param $question
* The question node.
* @param $answer
* The answer (dependent on question type).
*/
public function __construct($rid, $question, $answer);
/**
* Save the current response.
*/
public function save();
/**
* Delete the response.
*/
public function delete();
/**
* Calculate the score for the response.
* This MUST set the $score instance variable.
*/
public function score();
/**
* Determine whether the score is a passing score.
*/
public function isCorrect();
/**
* Indicate whether the response has been evaluated (scored) yet.
* Questions that require human scoring (e.g. essays) may need to manually
* toggle this.
*/
public function isEvaluated();
/**
* Get the user's response.
*/
public function getResponse();
/**
* Get data suitable for reporting a user's score on the question.
* This expects an object with the following attributes:
*
* answer_id; // The answer ID
* answer; // The full text of the answer
* is_evaluated; // 0 if the question has not been evaluated, 1 if it has
* score; // The score the evaluator gave the user; this should be 0 if is_evaluated is 0.
* question_vid
* question_nid
* result_id
*/
public function getReport();
/**
* Return an HTML marked-up report for displaying the results of this question.
*
* @return
* An HTML string.
*/
public function formatReport($showPoints, $showFeedback);
/**
* Get the integer score.
*/
public function getScore();
/**
* Repesent the response as a stdClass object.
*
* Convert data to an object that has the following properties:
* - $score
* - $rid
* - $nid
* - $vid
* - $is_correct
*/
public function toBareObject();
}
/**
* A base implementation of QuizQuestionResponse.
*
*/
abstract class AbstractQuizQuestionResponse {
// These are only public until we can
// adjust all of Quiz. DO NOT EXPECT THESE
// TO BE ACCESSIBLE!
public $score = 0;
public $rid = 0;
public $is_correct = FALSE;
protected $evaluated = TRUE;
protected $question = NULL;
protected $answer = NULL;
/*
public function __construct($rid, $question, $answer) {
$this->rid = $rid;
$this->question = $question;
$this->answer = $answer;
}
*/
public function isEvaluated() {
return $this->evaluated;
}
/**
* Check to see if the answer is marked as correct.
*
* This default version returns TRUE iff the score is equal to the maximum possible score.
*/
function isCorrect() {
$possible = _quiz_question_get_instance($this->question)->getMaximumScore();
$actual = $this->score;
return ($possible == $actual);
}
function getScore() {
return $this->score;
}
function toBareObject() {
$obj = new stdClass();
$obj->score = $this->score; // This can be 0 for unscored.
$obj->nid = $this->question->nid;
$obj->vid = $this->question->vid;
$obj->rid = $this->rid;
$obj->is_correct = $this->isCorrect();
$obj->is_evaluated = $this->isEvaluated();
$obj->is_skipped = FALSE;
return $obj;
}
public function getReport() {
// Basically, we encode internal information in a
// legacy array format for Quiz.
$report = array(
'answer_id' => 0, // <-- Stupid vestige of multichoice.
'answer' => $this->answer,
'is_evaluated' => $this->isEvaluated(),
'is_correct' => $this->isCorrect(),
'score' => $this->score,
'question_vid' => $this->question->vid,
'question_nid' => $this->question->nid,
'result_id' => $this->rid,
);
return $report;
}
}
; $Id$
name = Quiz Question
package = Quiz
description = Provides support for base-level question types.
core = 6.x
php = 5.2
dependencies[] = autoload
dependencies[] = quiz
<?php
// $Id$
/**
* The installer file for quiz_question.
* @file
*/
/**
* Implementation of hook_install().
*/
function quiz_question_install() {
drupal_install_schema('quiz_question');
}
/**
* Implementation of hook_uninstall().
*/
function quiz_question_uninstall() {
drupal_uninstall_schema('quiz_question');
}
/**
* Implementation of hook_schema().
*/
function quiz_question_schema() {
//////////////////////
// TRUE/FALSE tables.
//////////////////////
// Extensions to nodes for T/F
$schema['quiz_truefalse_node'] = array(
'fields' => array(
// Nid/vid of the question
'nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
// The actual answer the user gave. True or false.
'correct_answer' => array(
'type' => 'int',
'unsigned' => TRUE,
'size' => 'tiny',
'default' => 0,
),
'feedback' => array(
'type' => 'text',
),
),
'primary key' => array(
'vid','nid'
),
);
// Place to store user answers for T/F questions
$schema['quiz_truefalse_user_answers'] = array(
'fields' => array(
// Nid/vid of the question
'question_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'question_vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
// User's result set
'result_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
// The actual answer the user gave. True or false.
'answer' => array(
'type' => 'int',
'unsigned' => TRUE,
'size' => 'tiny',
'default' => 0,
),
'score' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array(
'result_id', 'question_nid', 'question_vid',
),
);
return $schema;
}
<?php
// $Id$
/**
* Quiz Question module.
* This module provides the basic facilities for adding quiz question types to a quiz.
* While you can create standard Quiz question types simply by implementing the appropriate
* hooks, this module provides a framework that makes adding new types much easier.
* @file
*/
/**
* Implementation of hook_help().
*/
function quiz_question_help($path, $args) {
if ($path == 'admin/help#quiz_quesion') {
return t('Support for Quiz question types.');
}
}
/**
* Implementation of hook_autoload_info().
*/
function quiz_question_autoload_info() {
return array(
// Base interfaces and classes:
'QuizQuestion' => array('file' => 'quiz_question.core.inc'),
'QuizQuestionResponse' => array('file' => 'quiz_question.core.inc'),
'AbstractQuizQuestionResponse' => array('file' => 'quiz_question.core.inc'),
// True/False question type:
'TrueFalseQuestion' => array('file' => 'quiz_question.truefalse.inc'),
'TrueFalseResponse' => array('file' => 'quiz_question.truefalse.inc'),
);
}
/**
* Implementation of hook_menu().
*/
function quiz_question_menu() {
$types = _quiz_question_get_implementations();
foreach ($types as $type => $definition) {
$items['admin/quiz/' . str_replace('_', '-', $types)] = array(
'title' => t('@name administration', array('@name' => $definition['name'])),
'description' => t('Configure the @name question type.', array('@name' => $definition['name'])),
'page callback' => 'drupal_get_form',
'page arguments' => array('quiz_question_type_settings_form', $type),
'access arguments' => array('configure quiz question types'),
'type' => MENU_NORMAL_ITEM, // MENU_CALLBACK, MENU_SUGGESTED_ITEM, MENU_LOCAL_TASK, MENU_DEFAULT_LOCAL_TASK
//'file' => 'module.admin.inc'
//'weight' => 0,
//'menu_name' => 'name of menu',
);
}
return $items;
}
/**
* Implementation of hook_perm().
*/
function quiz_question_perm() {
return array(
// Manage questions:
'create quiz question', 'edit own quiz question', 'edit any quiz question', 'delete own quiz question', 'delete any quiz question',
'score quiz question', 'configure quiz question types',
);
}
/**
* Implementation of hook_theme().
*/
function quiz_question_theme() {
return array(
'quiz_question_report' => array(
'arguments' => array('show_points' => NULL, 'show_feedback' => NULL),
'file' => 'quiz_question.theme.inc',
),
);
}
/**
* Implementation of hook_node_info().
*/
function quiz_question_node_info() {
$types = _quiz_question_get_implementations();
$info = array();
$defaults = array(
'module' => 'quiz_question',
//'help' => t('Add the question text and set a score for this question.'),
'has_body' => TRUE,
'has_title' => TRUE,
'body_label' => t('Question'),
);
foreach ($types as $type => $definition) {
$node_info = array(
'help' => t('Create a new @name.', array('@name' => $definition['name'])),
'name' => $definition['name'],
'description' => $definition['description']
);
$info[$type] = $node_info + $defaults;
}
return $info;
}
/**
* Implementation of hook_access().
*/
function quiz_question_access($op, $node, $account) {
// Allow admin to do whatever.
if (user_access('administer quiz', $account)) {
return TRUE;
}
switch ($op) {
case 'view':
return user_access('view quiz question outside of a quiz');
case 'create':
return user_access('create quiz question', $account);
case 'update':
if (user_access('edit any quiz question', $account) || user_access('edit own quiz question', $account) && ($account->uid == $node->uid)) {
return TRUE;
}
case 'delete':
if (user_access('delete any quiz question', $account) || user_access('delete own quiz question', $account) && ($account->uid == $node->uid)) {
return TRUE;
}
}
}
/**
* Implementation of hook_form().
*/
function quiz_question_form(&$node, $form_state) {
$form = array();
// First we do the basic title and text fields.
// XXX: Should there be a way for question types to modify these (other than form alter hooks)?
// Allow user to set title?
if (user_access('allow user titles')) {
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $node->title,
'#required' => FALSE,
'#description' => t('Add a title that will help distinguish this question from other questions. This will not be seen during the quiz.'),
);
}
else {
$form['title'] = array(
'#type' => 'value',
'#value' => $node->title,
);
}
$form['body'] = array(
'#type' => 'textarea',
'#title' => t('Question'),
'#description' => t('Enter the full text of the question that will be shown to the user. Include any special instructions on how to answer.'),
'#default_value' => $node->body,
'#required' => TRUE,
);
// Quiz ID used here to tie creation of a question to a specific quiz.
$quiz_id = arg(3);
if (!empty($quiz_id)) {
$quiz = node_load((int)$quiz_id);
$form['quiz_id'] = array(
'#type' => 'value',
'#value' => $quiz_id,
);
$form['quiz_vid'] = array(
'#type' => 'value',
'#value' => $quiz->vid,
);
}
$question = _quiz_question_get_instance($node);
$form = array_merge($form, $question->getCreationForm($form_state));
// If coming from quiz view, go back there on submit.
if (!empty($quiz_id)) {
$form['#redirect'] = 'node/'. $quiz_id .'/questions';
}
return $form;
}
/**
* Implementation of hook_validate().
*/
function quiz_question_validate($node, &$form) {
// Check to make sure that there is a question.
if (empty($node->body)) {
form_set_error('body', t('Question text is empty.'));
}
_quiz_question_get_instance($node)->validate($node, $form);
}
/**
* Get the form to show to the user.
*/
function quiz_question_question_form($context, $node) {
$form = _quiz_question_get_instance($node)->getQuestionForm($node, $context);
$quiz = menu_get_object(); // Get the quiz object.
if (!empty($quiz->backwards_navigation) && !empty($node->question_number)) {
$form['back'] = array(
'#type' => 'submit',
'#value' => t('Back'),
);
}
// Add navigation at the bottom:
// Submit button
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Next'),
);
/*
if (empty($node->no_skip_button)) {
$form['op'] = array(
'#type' => 'submit',
'#value' => t('Skip'),
);
}
*/
return $form;
}
/**
* Implementation of Quiz's hook_evaluate_question().
*
* @return
* Object with nid, vid, rid, score, is_correct flags set.
*/
function quiz_question_evaluate_question($question, $rid, $answer = NULL) {
if (empty($answer)) {
$answer = $_POST['tries'];
}
unset($_POST['tries']);
$response = _quiz_question_response_get_instance($rid, $question, $answer);
$response->score();
// If a rid is set, we are taking a quiz.
if ($rid && !empty($answer)) {
$response->save();
}
// Convert the response to a bare object.
return $response->toBareObject();
}
/**
* Implementation of quiz hook_skip_question().
*/
function quiz_question_skip_question($question, $rid) {
unset($_POST['tries']); // Unset any answer that might have been set.
// Delete any old answers for this question (for backwards nav).
_quiz_question_response_get_instance($rid, $question)->delete();
// This is the standard response:
$response = new stdClass();
$response->nid = $question->nid;
$response->vid = $question->vid;
$response->rid = $rid;
$response->is_skipped = TRUE;
return $response;
}
/**
* Implementation of hook_list_questions().
*/
function quiz_question_list_questions($count = 0, $offset = 0) {
$sql = "SELECT n.nid, n.vid, r.body, r.format
FROM {node} AS n
INNER JOIN {node_revisions} AS r USING(vid)
WHERE n.type IN (%s) ORDER BY n.type, n.changed";
$types = array();
foreach (array_keys(_quiz_question_get_implementations()) as $key) {
$types[] = "'" . $key . "'";
}
$type = implode(',', $types);
if ($count == 0) {
// Return all results
$result = db_query($sql, $type);
}
else {
// return only $count results
$result = db_query_range($sql, $type, $offset, $count);
}
$questions = array();
while ($question = db_fetch_object($result)) {
$question->question = check_markup($question->body, $question->format);
$questions[] = $question;
}
return $questions;
}
/**
* Implementation of hook_render_question().
*
* @param $node
* The question node.
*/
function quiz_question_render_question($node) {
return drupal_get_form('quiz_question_question_form', $node);
}
/**
* Imlementation of hook_get_report().
*
* @return
* Node containing all of the items from the question plus the user's answer.
*/
function quiz_question_get_report($nid, $vid, $rid) {
$node = node_load($nid, $vid);
$result = _quiz_question_response_get_instance($rid, $node)->getReport();
$node->answers[$result['answer_id']] = $result;
// If this has been evaluated, we mark it as correct.
// FIXME: This needs to be improved substantially.
//if ($result && $result['is_evaluated'] && $result['score'] > 0) {
// $node->correct = TRUE;
//}
$node->correct = $result['is_correct'];
return $node;
}
/**
* Implementation of hook_quiz_question_score().
*/
function quiz_question_quiz_question_score($quiz, $question_nid, $question_vid, $rid) {
$node = node_load($question_nid, $question_vid);
$response = _quiz_question_response_get_instance($rid, $node);
$question = _quiz_question_get_instance($node);
$response->score();
$score = new stdClass();
$score->possible = $question->getMaximumScore();
$score->attained = $response->getScore();
$score->is_evaluated = $response->isEvaluated();
return $score;
}
/**
* Get the admin settings form for a question type.
*/
function quiz_question_type_settings_form($context, $type) {
return _quiz_question_get_instance($type)->getAdminForm();
}
// NODE API
/**
* Implementation of hook_nodeapi().
*/
function quiz_question_nodeapi(&$node, $op) {
if($op == 'delete revision') {
_quiz_question_get_instance($node)->delete();
}
}
/**
* Implementation of hook_insert()
*/
function quiz_question_insert($node) {
_quiz_question_get_instance($node)->save(TRUE);
// If the form says this should be attached to a quiz, attach it.
if (isset($node->quiz_id) && $node->quiz_id > 0) {
$sql = 'INSERT INTO {quiz_node_relationship} (parent_nid, parent_vid, child_nid, child_vid, question_status)
VALUES (%d, %d, %d, %d, %d)';
db_query($sql, $node->quiz_id, $node->quiz_vid, $node->nid, $node->vid, QUESTION_ALWAYS);
}
}
/**
* Implementation of hook_view()
*/
function quiz_question_view($node, $teaser = FALSE, $page = FALSE) {
$node->content['body']['#value'] = drupal_get_form('quiz_question_question_form', $node);
return $node;
}
/**
* Implementation of hook_update().
*/
function quiz_question_update($node) {
_quiz_question_get_instance($node)->save();
}
/**
* Implementation of hook_delete().
*/
function quiz_question_delete(&$node) {
_quiz_question_get_instance($node)->delete();
}
/**
* Implementation of hook_load().
*/
function quiz_question_load($node) {
return _quiz_question_get_instance($node)->load();
}
// END NODE API
/**
* Get an instance of a quiz question.
* Get information about the class and use it to construct a new
* object of the appropriate type.
*/
function _quiz_question_get_instance($node) {
if (is_object($node)) {
$name = $node->type;
}
elseif (is_array($node)) {
$name = $node['type'];
}
else {
$name = $node;
}
$info = _quiz_question_get_implementations();
$constructor = $info[$name]['question provider'];
return new $constructor($node);
}
function _quiz_question_response_get_instance($rid, $question, $answer= NULL) {
$info = _quiz_question_get_implementations();
$constructor = $info[$question->type]['response provider'];
return new $constructor($rid, $question, $answer);
}
/**
* Get the information about various implementations of quiz questions.
*
* @param $reset
* If this is true, the cache will be reset.
* @return
* An array of information about quiz question implementations.
* @see quiz_question_quiz_question_info() for an example of a quiz question info hook.
*/
function _quiz_question_get_implementations($name = NULL, $reset = FALSE) {
static $info = array();
if (empty($info) || $reset) {
$qtypes = module_invoke_all('quiz_question_info');
foreach ($qtypes as $type => $definition) {
// We only want the ones with classes.
if (!empty($definition['question provider'])) {
$info[$type] = $definition;
}
}
}
return $info;
}
/**
* Implementation of hook_quiz_question_info().
*/
function quiz_question_quiz_question_info() {
return array(
'true_false' => array(
'name' => 'True/false question',
'description' => 'Quiz questions that allow a user to select true or false (yes/no).',
'question provider' => 'TrueFalseQuestion',
'response provider' => 'TrueFalseResponse',
'module' => 'quiz_question',
),
/*
'long_answer' => array(
'name' => 'Long answer question',
'description' => 'Quiz questions that allow a user to enter multiple paragraphs of text.',
'question provider' => 'LongAnswerQuestion',
'response provider' => 'LongAnswerResponse',
'module' => 'quiz_question',
),
*/
);
}
<?php
// $Id$
/**
* Themes for the quiz question module.
*/
/**
* Implementation of theme_$type_report().
* Theme the feedback report for a quiz. This report is generated (typically) at the end of the quiz.
*/
function theme_quiz_question_report($question, $show_points, $show_feedback) {
if (!isset($question->answers[0])) {
drupal_set_message('No result could be calculated.', 'status');
return;
}
$answer = $question->answers[0];
$types = _quiz_get_question_types();
$constructor = $types[$question->type]['response provider'];
$result = new $constructor($answer['result_id'], $question);
return $result->formatReport();
}
<?php
/**
* Defines the classes necessary for a True/False quiz.
*
* @file
*/
/**
* Implementation of QuizQuestion.
*/
class TrueFalseQuestion implements QuizQuestion {
/**
* The current node for this question.
*/
protected $node = NULL;
public function __construct($node) {
$this->node = $node;
}
public function save($is_new = FALSE) {
drupal_set_message('Save called', 'status');
if (!isset($this->node->feedback)) {
$this->node->feedback = '';
}
if ($is_new || $this->node->revision == 1) {
$sql = "INSERT INTO {quiz_truefalse_node} (nid, vid, correct_answer, feedback) VALUES (%d, %d, %d, '%s')";
db_query($sql, $this->node->nid, $this->node->vid, (int)$this->node->correct_answer, $this->node->feedback);
}
else {
drupal_set_message('Updating', 'status');
$sql = "UPDATE {quiz_truefalse_node} SET correct_answer = %d, feedback = '%s' WHERE nid = %d AND vid = %d";
db_query($sql, (int)$this->node->correct_answer, $this->node->feedback, $this->node->nid, $this->node->vid);
}
}
public function validate($node, &$form) {
// This space intentionally left blank. :)
}
public function delete($only_this_version = FALSE) {
// Only delete a nid/vid.
if ($only_this_version) {
$sql = 'DELETE FROM {quiz_truefalse_node} WHERE nid = %d AND vid = %d';
db_query($sql, $this->node->nid, $this->node->vid);
}
// Delete all versions of the quiz question.
else {
$sql = 'DELETE FROM {quiz_truefalse_node} WHERE nid = %d';
db_query($sql, $this->node->nid, $this->node->vid);
}
}
public function load() {
$sql = 'SELECT correct_answer, feedback FROM {quiz_truefalse_node} WHERE nid = %d AND vid = %d';
$result = db_fetch_object(db_query($sql, $this->node->nid, $this->node->vid));
return $result;
}
public function view() {
return $this->getQuestionForm($this->node);
}
// This is called whenever a question is rendered, either
// to an administrator or to a quiz taker.
public function getQuestionForm($node, $context = NULL) {
// Question first
$form['question'] = array(
'#type' => 'markup',
'#value' => $node->body
);
// 'tries' is unfortunately required by quiz.module
$form['tries'] = array(
'#type' => 'radios',
'#title' => t('Choose one'),
'#options' => array(
1 => t('True'),
0 => t('False'),
),
//'#default_value' => 1,
'#required' => TRUE,
);
return $form;
}
public function getAdminForm($edit = NULL) {
$form['settings'] = array(
'#type' => 'markup',
'#value' => t('There are no settings for this question type.'),
);
return $form;
}
public function getCreationForm($edit) {
$form['correct_answer'] = array(
'#type' => 'radios',
'#title' => t('Correct answer'),
'#options' => array(
1 => t('True'),
0 => t('False'),
),
'#default_value' => isset($node->correct_answer) ? $node->correct_answer : 1,
'#required' => TRUE,
);
$form['fiedback_fields'] = array(
'#type' => 'fieldset',
'#title' => t('Feedback Settings'),
'#description' => t('Settings pertaining to feedback given along with results.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['fiedback_fields']['feedback'] = array(
'#type' => 'textarea',
'#title' => t('Feedback Text'),
'#description' => t('Text to be displayed when the results are displayed'),
'#rows' => 5,
'#cols' => 60,
'#required' => FALSE,
);
return $form;
}
public function getMaximumScore() {
return 1;
}
/**
* Get the answer to this question.
*
* This is a utility function. It is not defined in the interface.
*/
public function getCorrectAnswer() {
$sql = "SELECT correct_answer FROM {quiz_truefalse_node} WHERE nid = %d AND vid = %d";
return db_result(db_query($sql, $this->node->nid, $this->node->vid));
}
}
class TrueFalseResponse extends AbstractQuizQuestionResponse {
public function __construct($rid, $question, $answer = NULL) {
$this->rid = $rid;
$this->question = $question;
if (!isset($answer)) {
$sql = "SELECT answer, score FROM {quiz_truefalse_user_answers} WHERE question_vid = %d AND result_id = %d";
$r = db_fetch_object(db_query($sql, $question->vid, $rid));
if (!empty($r)) {
$this->answer = $r->answer;
$this->score = $r->score;
}
}
else {
$this->answer = $answer;
}
}
public function save() {
$sql = "INSERT INTO {quiz_truefalse_user_answers} (question_nid, question_vid, result_id, answer, score)
VALUES (%d, %d, %d, %d, %d)";
db_query($sql, $this->question->nid, $this->question->vid, $this->rid, (int)$this->answer, (int)$this->score);
}
public function delete() {
$sql = 'DELETE FROM {quiz_truefalse_user_answers} WHERE question_nid = %d AND question_vid = %d AND result_id = %d';
db_query($sql, $this->question->nid, $this->question->vid, $this->rid);
}
public function score() {
$tfQuestion = new TrueFalseQuestion($this->question);
$this->score = (int)($this->getResponse() == $tfQuestion->getCorrectAnswer());
return $this->score;
}
public function getResponse() {
return $this->answer;
}
public function formatReport($showpoints = TRUE, $showfeedback = TRUE) {
// Build the question answers header (add blank space for IE).
if ($showpoints) {
$innerheader[] = t('Correct Answer');
}
$innerheader[] = t('User Answer');
if ($showfeedback) {
$innerheader[] = '&nbsp;';
}
if (empty($this->question->answers)) {
return t('Missing question.');
}
$answer = $this->question->answers[0];
$correct_answer = $answer['is_correct'] ? $answer['answer'] : !$answer['answer'];
$user_answer = $answer['answer'];
if ($showpoints) {
$rows[0][] = ($correct_answer ? t('True') : t('False'));
}
$rows[0][] = ($user_answer ? t('True') : t('False'));
if($showfeedback && !empty($this->question->feedback)) {
$rows[0][] = $this->question->feedback;
}
// Add the cell with the question and the answers.
$q_output = '<div class="quiz_summary_question"><span class="quiz_question_bullet">Q:</span> '. check_markup($this->question->body) .'</div>';
$q_output .= theme('table', $innerheader, $rows) .'<br />';
return $q_output;
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment