You are here

abstract class QuizQuestion in Quiz 7.6

Same name and namespace in other branches
  1. 6.4 question_types/quiz_question/quiz_question.core.inc \QuizQuestion
  2. 7 question_types/quiz_question/quiz_question.core.inc \QuizQuestion
  3. 7.4 question_types/quiz_question/quiz_question.core.inc \QuizQuestion
  4. 7.5 question_types/quiz_question/quiz_question.core.inc \QuizQuestion

A base implementation of a quiz_question, adding a layer of abstraction between the node API, quiz API and the question types.

It is required that Question types extend this abstract class.

This class has default behaviour that all question types must have. It also handles the node API, but gives the question types oppurtunity to save, delete and provide data specific to the question types.

This abstract class also declares several abstract functions forcing question-types to implement required methods.

Hierarchy

Expanded class hierarchy of QuizQuestion

File

question_types/quiz_question/quiz_question.core.inc, line 39
Classes used in the Quiz Question module.

View source
abstract class QuizQuestion {

  /*
   * QUESTION IMPLEMENTATION FUNCTIONS
   *
   * This part acts as a contract(/interface) between the question-types and the rest of the system.
   *
   * Question types are made by extending these generic methods and abstract methods.
   */

  /**
   * The current node for this question.
   */
  public $node = NULL;

  // Extra node properties
  public $nodeProperties = NULL;

  /**
   * QuizQuestion constructor stores the node object.
   *
   * @param $node
   *   The node object
   */
  public function __construct(stdClass &$node) {
    $this->node = $node;
  }

  /**
   * Allow question types to override the body field title
   *
   * @return
   *  The title for the body field
   */
  public function getBodyFieldTitle() {
    return t('Question');
  }

  /**
   * Returns a node form to quiz_question_form
   *
   * Adds default form elements, and fetches question type specific elements from their
   * implementation of getCreationForm
   *
   * @param array $form_state
   * @return unknown_type
   */
  public function getNodeForm(array &$form_state = NULL) {
    global $user;
    $form = array();

    // mark this form to be processed by quiz_form_alter. quiz_form_alter will among other things
    // hide the revion fieldset if the user don't have permission to controll the revisioning manually.
    $form['#quiz_check_revision_access'] = TRUE;

    // Allow user to set title?
    if (user_access('edit question titles')) {
      $form['helper']['#theme'] = 'quiz_question_creation_form';
      $form['title'] = array(
        '#type' => 'textfield',
        '#title' => t('Title'),
        '#maxlength' => 255,
        '#default_value' => $this->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.', array(
          '@quiz' => QUIZ_NAME,
        )),
      );
    }
    else {
      $form['title'] = array(
        '#type' => 'value',
        '#value' => $this->node->title,
      );
    }

    // Store quiz id in the form
    $form['quiz_nid'] = array(
      '#type' => 'hidden',
    );
    $form['quiz_vid'] = array(
      '#type' => 'hidden',
    );
    if (isset($_GET['quiz_nid']) && isset($_GET['quiz_vid'])) {
      $form['quiz_nid']['#value'] = intval($_GET['quiz_nid']);
      $form['quiz_vid']['#value'] = intval($_GET['quiz_vid']);
    }

    // Identify this node as a quiz question type so that it can be recognized by other modules effectively.
    $form['is_quiz_question'] = array(
      '#type' => 'value',
      '#value' => TRUE,
    );
    if (!empty($this->node->nid)) {
      if ($properties = entity_load('quiz_question', FALSE, array(
        'nid' => $this->node->nid,
        'vid' => $this->node->vid,
      ))) {
        $quiz_question = reset($properties);
      }
    }
    $form['feedback'] = array(
      '#type' => 'text_format',
      '#title' => t('Question feedback'),
      '#default_value' => !empty($quiz_question->feedback) ? $quiz_question->feedback : '',
      '#format' => !empty($quiz_question->feedback_format) ? $quiz_question->feedback_format : filter_default_format(),
      '#description' => t('This feedback will show when configured and the user answers a question, regardless of correctness.'),
    );

    //Add question type specific content
    $form = array_merge($form, $this
      ->getCreationForm($form_state));
    if ($this
      ->hasBeenAnswered()) {
      $log = t('The current revision has been answered. We create a new revision so that the reports from the existing answers stays correct.');
      $this->node->revision = 1;
      $this->node->log = $log;
    }
    return $form;
  }

  /**
   * Retrieve information relevant for viewing the node.
   *
   * (This data is generally added to the node's extra field.)
   *
   * @return
   *  Content array
   */
  public function getNodeView() {
    $type = node_type_get_type($this->node);
    $content['question_type'] = array(
      '#markup' => '<div class="question_type_name">' . $type->name . '</div>',
      '#weight' => -2,
    );

    /*
     $question_body = field_get_items('node', $this->node, 'body');
     $content['question'] = array(
     '#markup' => '<div class="question-body">' . $question_body[0]['safe_value'] . '</div>',
     '#weight' => -1,
     );
    */
    return $content;
  }

  /**
   * Getter function returning properties to be loaded when the node is loaded.
   *
   * @see load hook in quiz_question.module (quiz_question_load)
   *
   * @return array
   */
  public function getNodeProperties() {
    if (isset($this->nodeProperties)) {
      return $this->nodeProperties;
    }
    $props['max_score'] = db_query('SELECT max_score
            FROM {quiz_question_properties}
            WHERE nid = :nid AND vid = :vid', array(
      ':nid' => $this->node->nid,
      ':vid' => $this->node->vid,
    ))
      ->fetchField();
    $props['is_quiz_question'] = TRUE;
    $this->nodeProperties = $props;
    return $props;
  }

  /**
   * 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.
   *
   * 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.
   *
   * @see hook_update and hook_insert in quiz_question.module
   *
   * @param $is_new
   *  TRUE when the node is initially created.
   */
  public function save($is_new = FALSE) {

    // We call the abstract function saveNodeProperties to save type specific data
    $this
      ->saveNodeProperties($this->node->is_new);
    db_merge('quiz_question_properties')
      ->key(array(
      'nid' => $this->node->nid,
      'vid' => $this->node->vid,
    ))
      ->fields(array(
      'nid' => $this->node->nid,
      'vid' => $this->node->vid,
      'max_score' => $this
        ->getMaximumScore(),
      'feedback' => !empty($this->node->feedback['value']) ? $this->node->feedback['value'] : '',
      'feedback_format' => !empty($this->node->feedback['format']) ? $this->node->feedback['format'] : filter_default_format(),
    ))
      ->execute();

    // Save what quizzes this question belongs to.
    // @kludge the quiz nid/vid are still on the node
    if (!empty($this->node->quiz_nid)) {
      $this
        ->saveRelationships($this->node->quiz_nid, $this->node->quiz_vid);
    }
    if (!empty($this->node->revision)) {
      if (user_access('manual quiz revisioning') && !variable_get('quiz_auto_revisioning', 1)) {
        unset($_GET['destination']);
        unset($_REQUEST['edit']['destination']);
        drupal_goto("node/{$this->node->nid}/question-revision-actions");
      }
    }
  }

  /**
   * Delete question data from the database.
   *
   * Called by quiz_question_delete (hook_delete).
   * Child classes must call super
   *
   * @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) {

    // Delete answeres
    $delete = db_delete('quiz_node_results_answers')
      ->condition('question_nid', $this->node->nid);
    if ($only_this_version) {
      $delete
        ->condition('question_vid', $this->node->vid);
    }
    $delete
      ->execute();

    // Delete properties
    $delete = db_delete('quiz_question_properties')
      ->condition('nid', $this->node->nid);
    if ($only_this_version) {
      $delete
        ->condition('vid', $this->node->vid);
    }
    $delete
      ->execute();
  }

  /**
   * 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 $form
   *  The processed form.
   */
  public abstract function validateNode(array &$form);

  /**
   * Get the form through which the user will answer the question.
   *
   * @param $form_state
   *  The FAPI form_state array
   * @param $result_id
   *  The result id.
   * @return
   *  Must return a FAPI array.
   */
  public function getAnsweringForm(array $form_state = NULL, $result_id) {
    $form = array();
    $form['#element_validate'] = array(
      'quiz_question_element_validate',
    );
    return $form;
  }

  /**
   * Get the form used to create a new question.
   *
   * @param
   *  FAPI form state
   * @return
   *  Must return a FAPI array.
   */
  public abstract function getCreationForm(array &$form_state = NULL);

  /**
   * Get the maximum possible score for this question.
   */
  public abstract function getMaximumScore();

  /**
   * Save question type specific node properties
   */
  public abstract function saveNodeProperties($is_new = FALSE);

  /**
   * Save this Question to the specified Quiz.
   */
  function saveRelationships($nid, $vid) {
    $quiz_node = node_load($nid, $vid);
    if (quiz_has_been_answered($quiz_node)) {

      // We need to revise the quiz node if it has been answered
      $quiz_node->revision = 1;
      $quiz_node->auto_created = TRUE;
      node_save($quiz_node);
      drupal_set_message(t('New revision has been created for the @quiz %n', array(
        '%n' => $quiz_node->title,
        '@quiz' => QUIZ_NAME,
      )));
    }
    $insert_values = array();
    $insert_values['parent_nid'] = $quiz_node->nid;
    $insert_values['parent_vid'] = $quiz_node->vid;
    $insert_values['child_nid'] = $this->node->nid;
    $insert_values['child_vid'] = $this->node->vid;
    $insert_values['max_score'] = $this
      ->getMaximumScore();
    $insert_values['auto_update_max_score'] = $this
      ->autoUpdateMaxScore() ? 1 : 0;
    $insert_values['weight'] = 1 + db_query('SELECT MAX(weight) FROM {quiz_node_relationship} WHERE parent_vid = :vid', array(
      ':vid' => $quiz_node->vid,
    ))
      ->fetchField();
    $randomization = db_query('SELECT randomization FROM {quiz_node_properties} WHERE nid = :nid AND vid = :vid', array(
      ':nid' => $quiz_node->nid,
      ':vid' => $quiz_node->vid,
    ))
      ->fetchField();
    $insert_values['question_status'] = $randomization == 2 ? QUIZ_QUESTION_RANDOM : QUIZ_QUESTION_ALWAYS;
    entity_create('quiz_question_relationship', $insert_values)
      ->save();

    // Update max_score for relationships if auto update max score is enabled
    // for question
    $quizzes_to_update = array();
    $result = db_query('SELECT parent_vid as vid from {quiz_node_relationship} where child_nid = :nid and child_vid = :vid and auto_update_max_score=1', array(
      ':nid' => $this->node->nid,
      ':vid' => $this->node->vid,
    ));
    foreach ($result as $record) {
      $quizzes_to_update[] = $record->vid;
    }
    db_update('quiz_node_relationship')
      ->fields(array(
      'max_score' => $this
        ->getMaximumScore(),
    ))
      ->condition('child_nid', $this->node->nid)
      ->condition('child_vid', $this->node->vid)
      ->condition('auto_update_max_score', 1)
      ->execute();
    quiz_update_max_score_properties($quizzes_to_update);
    quiz_update_max_score_properties(array(
      $quiz_node->vid,
    ));
  }

  /**
   * Finds out if a question has been answered or not
   *
   * This function also returns TRUE if a quiz that this question belongs to have been answered.
   * Even if the question itself haven't been answered. This is because the question might have
   * been rendered and a user is about to answer it...
   *
   * @return
   *   true if question has been answered or is about to be answered...
   */
  public function hasBeenAnswered() {
    if (!isset($this->node->vid)) {
      return FALSE;
    }
    $answered = db_query_range('SELECT 1 FROM {quiz_node_results} qnres
            JOIN {quiz_node_relationship} qnrel ON (qnres.vid = qnrel.parent_vid)
            WHERE qnrel.child_vid = :child_vid', 0, 1, array(
      ':child_vid' => $this->node->vid,
    ))
      ->fetch();
    return $answered ? TRUE : FALSE;
  }

  /**
   * Determines if the user can view the correct answers
   *
   * @todo grabbing the node context here probably isn't a great idea
   *
   * @return boolean
   *   true iff the view may include the correct answers to the question
   */
  public function viewCanRevealCorrect() {
    global $user;
    $quiz_node = node_load(arg(1));
    $reveal_correct[] = user_access('view any quiz question correct response');
    $reveal_correct[] = $user->uid == $this->node->uid;
    if (array_filter($reveal_correct)) {
      return TRUE;
    }
  }

  /**
   * Utility function that returns the format of the node body
   */
  protected function getFormat() {
    $node = isset($this->node) ? $this->node : $this->question;
    $body = field_get_items('node', $node, 'body');
    return isset($body[0]['format']) ? $body[0]['format'] : NULL;
  }

  /**
   * This may be overridden in subclasses. If it returns true,
   * it means the max_score is updated for all occurrences of
   * this question in quizzes.
   */
  protected function autoUpdateMaxScore() {
    return FALSE;
  }
  public function getAnsweringFormValidate(array &$form, array &$form_state = NULL) {
  }

  /**
   * Is this question graded?
   *
   * Questions like Quiz Directions, Quiz Page, and Scale are not.
   *
   * By default, questions are expected to be gradeable
   *
   * @return bool
   */
  public function isGraded() {
    return TRUE;
  }

  /**
   * Does this question type give feedback?
   *
   * Questions like Quiz Directions and Quiz Pages do not.
   *
   * By default, questions give feedback
   *
   * @return bool
   */
  public function hasFeedback() {
    return TRUE;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
QuizQuestion::$node public property The current node for this question.
QuizQuestion::$nodeProperties public property
QuizQuestion::autoUpdateMaxScore protected function This may be overridden in subclasses. If it returns true, it means the max_score is updated for all occurrences of this question in quizzes.
QuizQuestion::delete public function Delete question data from the database. 6
QuizQuestion::getAnsweringForm public function Get the form through which the user will answer the question. 8
QuizQuestion::getAnsweringFormValidate public function 6
QuizQuestion::getBodyFieldTitle public function Allow question types to override the body field title 4
QuizQuestion::getCreationForm abstract public function Get the form used to create a new question. 8
QuizQuestion::getFormat protected function Utility function that returns the format of the node body
QuizQuestion::getMaximumScore abstract public function Get the maximum possible score for this question. 8
QuizQuestion::getNodeForm public function Returns a node form to quiz_question_form
QuizQuestion::getNodeProperties public function Getter function returning properties to be loaded when the node is loaded. 6
QuizQuestion::getNodeView public function Retrieve information relevant for viewing the node. 6
QuizQuestion::hasBeenAnswered public function Finds out if a question has been answered or not
QuizQuestion::hasFeedback public function Does this question type give feedback? 2
QuizQuestion::isGraded public function Is this question graded? 2
QuizQuestion::save public function 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.
QuizQuestion::saveNodeProperties abstract public function Save question type specific node properties 8
QuizQuestion::saveRelationships function Save this Question to the specified Quiz.
QuizQuestion::validateNode abstract public function Provides validation for question before it is created. 8
QuizQuestion::viewCanRevealCorrect public function Determines if the user can view the correct answers
QuizQuestion::__construct public function QuizQuestion constructor stores the node object. 1