You are here

quiz_question.module in Quiz 6.6

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

question_types/quiz_question/quiz_question.module
View source
<?php

/**
 * 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',
    ),
  );
}

/**
 * Implementation of hook_menu().
 */
function quiz_question_menu() {

  // Menu item for seeing references to a question.
  $items['node/%quiz_question_type_access/references'] = array(
    'title' => t('References'),
    'page callback' => 'quiz_question_references',
    'page arguments' => array(
      1,
    ),
    // FIXME give this a sensible callback
    'access callback' => TRUE,
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz_question.pages.inc',
  );

  // Menu items for admin view of each question type.
  $types = _quiz_question_get_implementations();
  foreach ($types as $type => $definition) {
    $items['admin/quiz/' . str_replace('_', '-', $type)] = 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,
    );
  }
  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',
    ),
    'quiz_question_creation_form' => array(
      'arguments' => array(
        'form' => 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();
  $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,
    '#weight' => -15,
  );

  // process up the filter format
  $form['format'] = filter_form($node->format);

  // 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['helper']['#theme'] = 'quiz_question_creation_form';
    $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,
    );
  }

  // Collection ID used here to tie creation of a question to a specific quiz or qcollection
  $collection_id = arg(3);
  if (!empty($collection_id)) {
    $collection = node_load((int) $collection_id);
    $form['collection_nid'] = array(
      '#type' => 'value',
      '#value' => $collection->nid,
    );
    $form['collection_vid'] = array(
      '#type' => 'value',
      '#value' => $collection->vid,
    );

    // If coming from collection view, go back there on submit.
    // TODO ask the node itself where to go after authoring a question
    switch ($collection->type) {
      case 'quiz':
        $form['#redirect'] = 'node/' . $collection->nid . '/questions';
        break;
      case 'qcollection':
        $form['#redirect'] = 'node/' . $collection->nid . '/items';
        break;
    }
  }
  $question = _quiz_question_get_instance($node);
  $form = array_merge($form, $question
    ->getCreationForm($form_state));
  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);
  if (_quiz_is_taking_context()) {
    $quiz = menu_get_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'),
    );

    /* TODO is skipping supporting?
       if (empty($node->no_skip_button)) {
       $form['op']  = array(
       '#type' => 'submit',
       '#value' => t('Skip'),
       );
       }
       */
  }
  else {

    // reconstruct form so type is first (at top)
    // TODO is there a better way?  like unshift an assoc tuple?
    $type = node_get_types('type', $node);
    $newform['question_type'] = array(
      '#type' => 'markup',
      '#value' => '<div class="question_type_name">' . $type->name . '</div>',
    );
    $newform += $form;
    $form = $newform;
    if (!user_access('view quiz question solutions')) {
      $form['tries'] = array(
        '#type' => 'markup',
        '#value' => '<br /> <em>Answers hidden</em>.',
      );
    }
  }
  return $form;
}

/**
 * Form for teaser display
 */
function quiz_question_teaser_form($context, $node) {
  $form['question'] = array(
    '#type' => 'markup',
    '#value' => check_markup($node->body, $node->format, FALSE),
  );

  // reconstruct form so type is first (at top)
  // TODO is there a better way?  like unshift an assoc tuple?
  $type = node_get_types('type', $node);
  $newform['question_type'] = array(
    '#type' => 'markup',
    '#value' => '<div class="question_type_name">' . $type->name . '</div>',
  );
  $newform += $form;
  $form = $newform;
  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) {

  //print_r($_POST['tries']);exit;
  if (empty($answer)) {
    $answer = $_POST['tries'];

    //$answer = is_array($_POST['tries']) ? $_POST['tries'] : array($_POST['tries']);
  }
  unset($_POST['tries']);
  $response = _quiz_question_response_get_instance($rid, $question, $answer);

  // If a rid is set, we are taking a quiz.
  if ($rid && isset($answer)) {

    // We don't know whether or not the user has gone back a question. However,
    // we do know that deleting a question for this result set should be safe in
    // the case where the user has not gone back (since there will be no entries
    // to delete). So we always run the delete.
    $response
      ->delete();
    $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\n    FROM {node} AS n\n    INNER JOIN {node_revisions} AS r USING(vid)\n    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);
  $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) {
  $node = new stdClass();
  $node->type = $type;
  return _quiz_question_get_instance($node)
    ->getAdminForm();
}

// NODE API

/**
 * Implementation of hook_nodeapi().
 */
function quiz_question_nodeapi(&$node, $op) {
  if ($op == 'delete revision') {
    _quiz_question_get_instance($node)
      ->delete();
  }
  elseif ($op == 'presave') {
    $q_types = _quiz_question_get_implementations();
    foreach ($q_types as $q_type => $info) {
      if ($node->type == $q_type) {
        if (drupal_strlen($node->title == 0)) {
          $node->title = drupal_substr(strip_tags($node->body), 0, variable_get('quiz_autotitle_length', 50));
        }
      }
    }
  }
}

/**
 * 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) {
  if ($teaser) {
    $form_markup = drupal_get_form('quiz_question_teaser_form', $node);
  }
  else {
    $form_markup = drupal_get_form('quiz_question_question_form', $node);
  }
  $node->content['body']['#value'] = $form_markup;
  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;
}

/**
 * Load a quiz question node and validate it.
 *
 * @param $arg
 *  The Node ID
 * @return
 *  A quiz question node object or FALSE if a load failed.
 */
function quiz_question_type_access_load($arg) {
  $node = node_load($arg);
  if (!$node) {
    return FALSE;
  }
  $qtypes = module_invoke_all('quiz_question_info');
  $qtypes_key = array_keys($qtypes);
  if (!in_array($node->type, $qtypes_key)) {
    return FALSE;
  }
  return $node;
}

Functions

Namesort descending Description
quiz_question_access Implementation of hook_access().
quiz_question_autoload_info Implementation of hook_autoload_info().
quiz_question_delete Implementation of hook_delete().
quiz_question_evaluate_question Implementation of Quiz's hook_evaluate_question().
quiz_question_form Implementation of hook_form().
quiz_question_get_report Imlementation of hook_get_report().
quiz_question_help Implementation of hook_help().
quiz_question_insert Implementation of hook_insert()
quiz_question_list_questions Implementation of hook_list_questions().
quiz_question_load Implementation of hook_load().
quiz_question_menu Implementation of hook_menu().
quiz_question_nodeapi Implementation of hook_nodeapi().
quiz_question_node_info Implementation of hook_node_info().
quiz_question_perm Implementation of hook_perm().
quiz_question_question_form Get the form to show to the user.
quiz_question_quiz_question_score Implementation of hook_quiz_question_score().
quiz_question_render_question Implementation of hook_render_question().
quiz_question_skip_question Implementation of quiz hook_skip_question().
quiz_question_teaser_form Form for teaser display
quiz_question_theme Implementation of hook_theme().
quiz_question_type_access_load Load a quiz question node and validate it.
quiz_question_type_settings_form Get the admin settings form for a question type.
quiz_question_update Implementation of hook_update().
quiz_question_validate Implementation of hook_validate().
quiz_question_view Implementation of hook_view()
_quiz_question_get_implementations Get the information about various implementations of quiz questions.
_quiz_question_get_instance Get an instance of a quiz question. Get information about the class and use it to construct a new object of the appropriate type.
_quiz_question_response_get_instance