You are here

quiz.pages.inc in Quiz 6.4

User pages.

File

quiz.pages.inc
View source
<?php

/**
 * User pages.
 * @file
 */

/**
 * Show result page for a given result id
 *
 * @param $result_id
 *  Result id
 */
function quiz_user_results($result_id) {
  global $user;
  $sql = 'SELECT qnp.nid, qnp.vid, qnrs.uid
    FROM {quiz_node_properties} qnp
    INNER JOIN {quiz_node_results} qnrs ON qnrs.vid = qnp.vid
    WHERE qnrs.result_id = %d';
  $result = db_fetch_object(db_query($sql, $result_id));
  if ($result->nid) {

    // User can view own results (quiz_menu sets access to 'own results').
    // User with role 'user results' can view other user's results.
    if ($result->uid != $user->uid && !user_access('view any quiz results')) {
      drupal_access_denied();
      return;
    }
    $quiz = node_load($result->nid, $result->vid);
    $questions = _quiz_get_answers($quiz, $result_id);
    $score = quiz_calculate_score($quiz, $result_id);
    $summary = _quiz_get_summary_text($quiz, $score);
    return theme('quiz_user_summary', $quiz, $questions, $score, $summary);
  }
  else {
    drupal_not_found();
  }
}

/**
 * Form for showing feedback, and for editing the feedback if necessary...
 *
 * @param $form_state
 *   FAPI form state(array)
 * @param $questions
 *   array of questions to inclide in the report
 * @param $showpoints
 *   Should points be included in the report? (Boolean)
 * @param $showfeedback
 *   Should feedback be included in the report? (Boolean)
 * @param $allow_scoring
 *   Should we allow the user to score results that needs manual scoring? (Boolean)
 * @return $form
 *   FAPI form array
 */
function quiz_report_form($form_state, $questions, $showpoints = TRUE, $showfeedback = TRUE, $allow_scoring = FALSE) {
  $form = array();

  // The submit button is only shown if one or more of the questions has input elements
  $show_submit = FALSE;
  foreach ($questions as $question) {
    $module = quiz_question_module_for_type($question->type);
    if (!$module) {
      return array();
    }
    $function = $module . '_report_form';
    $form_to_add = $function($question, $showpoints, $showfeedback, $allow_scoring);
    if (isset($form_to_add['submit'])) {
      $show_submit = TRUE;
    }
    $form[] = $form_to_add;
  }
  $form['#theme'] = 'quiz_report_form';
  $form['#showpoints'] = $showpoints;
  $form['#showfeedback'] = $showfeedback;
  $form['#tree'] = TRUE;
  if ($show_submit) {
    $form['submit'] = array(
      '#type' => 'submit',
      '#submit' => array(
        'quiz_report_form_submit',
      ),
      '#validate' => array(
        'quiz_report_form_validate',
      ),
      '#value' => t('Submit'),
    );
  }
  return $form;
}

/**
 * Validate the report form
 */
function quiz_report_form_validate($form, &$form_state) {

  /* We go through the form state values and validates all
   * questiontypes with validation functions declared.
   */
  foreach ($form_state['values'] as $key => $q_values) {

    // Questions has numeric keys in the report form
    if (!is_numeric($key)) {
      continue;
    }

    // Questions store the name of the validation function with the key 'validate'
    if (!isset($q_values['validate'])) {
      continue;
    }

    // The validation function must exist
    if (!function_exists($q_values['validate'])) {
      continue;
    }

    // We call the validation function provided by the question
    call_user_func($q_values['validate'], $q_values, $key);
  }
}

/**
 * Submit the report form
 */
function quiz_report_form_submit($form, &$form_state) {

  /* We go through the form state values and submit all
   * questiontypes with validation functions declared.
   */
  foreach ($form_state['values'] as $key => $q_values) {

    // Questions has numeric keys in the report form
    if (!is_numeric($key)) {
      continue;
    }

    // Questions store the name of the validation function with the key 'submit'
    if (!isset($q_values['submit'])) {
      continue;
    }

    // The submit function must exist
    if (!function_exists($q_values['submit'])) {
      continue;
    }

    // Load the quiz
    if (!isset($quiz)) {
      $sql = 'SELECT nid, vid FROM {quiz_node_results} WHERE result_id = %d';
      $result = db_fetch_object(db_query($sql, $q_values['rid']));
      $quiz = node_load($result->nid, $result->vid);
      $rid = $q_values['rid'];
    }
    $q_values['quiz'] = $quiz;

    // We call the submit function provided by the question
    call_user_func($q_values['submit'], $q_values);
  }

  // Scores may have been changed. We take the necessary actions
  quiz_update_total_score_fast($rid, $quiz->vid);
  db_query('UPDATE {quiz_node_results} SET is_evaluated = 1 WHERE result_id = %d', $rid);
  $results_got_deleted = _quiz_maintain_results($quiz, $rid);

  // A message saying the quiz is unscored has already been set. We unset it here...
  _quiz_remove_unscored_message();

  // Notify the user if results got deleted as a result of him scoring an answer.
  $add = $quiz->quiz_keep_results == QUIZ_KEEP_BEST && $results_got_deleted ? ' ' . t('Note that this quiz is set to only keep each users best answer.') : '';
  $score_data = quiz_get_score_array($rid, $quiz->vid, TRUE);
  module_invoke_all('quiz_scored', $quiz, $score_data, $rid);
  drupal_set_message(t('The scoring data you provided has been saved.') . $add);
  $form_state['redirect'] = 'node/' . $quiz->nid . '/results';
}

/**
 * Helper function to remove the message saying the quiz haven't been scored
 */
function _quiz_remove_unscored_message() {
  if (is_array($_SESSION['messages']['warning'])) {

    // Search for the message, and remove it if we find it.
    foreach ($_SESSION['messages']['warning'] as $key => $val) {
      if ($val == t('This quiz has not been scored yet.')) {
        unset($_SESSION['messages']['warning'][$key]);
      }
    }

    // Clean up if the message array was left empty
    if (empty($_SESSION['messages']['warning'])) {
      unset($_SESSION['messages']['warning']);
      if (empty($_SESSION['messages'])) {
        unset($_SESSION['messages']);
      }
    }
  }
}

/**
 * Updates the total score using only one mySql query.
 *
 * @param $rid
 *  Result id
 * @param $quiz_vid
 *  Quiz node version id
 */
function quiz_update_total_score_fast($rid, $quiz_vid) {
  $sql = 'UPDATE {quiz_node_results} r
          SET r.score = ROUND(
            100 * (
              SELECT SUM(a.points_awarded)
              FROM {quiz_node_results_answers} a
              WHERE a.result_id = %d
            ) / (
              SELECT max_score
              FROM {quiz_node_properties} qnp
              WHERE vid = %d
            )
          )
          WHERE r.result_id = %d';
  db_query($sql, $rid, $quiz_vid, $rid);
}

/**
 * Returns an array of score information for a quiz
 *
 * @param unknown_type $rid
 * @param unknown_type $quiz_vid
 * @param unknown_type $is_evaluated
 */
function quiz_get_score_array($rid, $quiz_vid, $is_evaluated) {
  $sql = 'SELECT max_score, number_of_random_questions
          FROM {quiz_node_properties}
          WHERE vid = %d';
  $properties = db_fetch_object(db_query($sql, $quiz_vid));
  $sql = 'SELECT SUM(points_awarded)
          FROM (quiz_node_results_answers)
          WHERE result_id = %d';
  $total_score = db_result(db_query($sql, $rid));
  return array(
    'question_count' => $properties->number_of_random_questions + _quiz_get_num_always_questions($quiz_vid),
    'possible_score' => $properties->max_score,
    'numeric_score' => $total_score,
    'percentage_score' => $properties->max_score == 0 ? 0 : round($total_score * 100 / $properties->max_score),
    'is_evaluated' => $is_evaluated,
  );
}

/**
 * Displays all the quizzes the user has taken part in.
 *
 * @param $user_id
 *  User id
 * @return
 *  HTML output for page.
 */
function quiz_get_user_results($user_id) {
  $results = array();
  $sql = "SELECT n.nid, n.title, qnp.pass_rate, qnrs.result_id, qnrs.time_start, qnrs.time_end, qnrs.score, qnrs.is_evaluated\n    FROM {node} n\n    INNER JOIN {quiz_node_properties} qnp ON n.nid = qnp.nid\n    INNER JOIN {quiz_node_results} qnrs ON qnrs.vid = qnp.vid\n    INNER JOIN {users} u ON u.uid = qnrs.uid\n    WHERE n.type = 'quiz'\n      AND u.uid = %d\n      AND qnrs.time_end > 0\n    ORDER BY qnrs.result_id ASC";
  $dbresult = db_query($sql, $user_id);

  // Create an array out of the results.
  while ($line = db_fetch_array($dbresult)) {
    $results[$line['result_id']] = $line;
  }
  return theme('quiz_get_user_results', $results);
}

/**
 * Show results for the current quiz
 */
function quiz_my_results($node) {
  global $user;
  $results = array();
  $res = db_query("\n    SELECT qnp.nid, qnp.pass_rate, qnrs.result_id, qnrs.time_start, qnrs.time_end, qnrs.score\n    FROM {quiz_node_properties} qnp\n    INNER JOIN {quiz_node_results} qnrs ON qnrs.nid = qnp.nid\n    WHERE qnrs.uid = %d AND qnrs.nid = %d AND qnrs.is_evaluated = 1\n    ORDER BY qnrs.result_id DESC\n  ", $user->uid, $node->nid);

  // Create an array out of the results.
  while ($line = db_fetch_array($res)) {
    $results[$line['result_id']] = $line;
  }
  return theme('quiz_my_results_for_quiz', $results);
}

// THEME FUNCTIONS

/**
 * Theme the user results page.
 *
 * @param $results
 *  An array of quiz information.
 * @return
 *  Themed html.
 *
 * @ingroup themeable
 */
function theme_quiz_get_user_results($results) {
  $rows = array();
  while (list($key, $result) = each($results)) {
    $interval = _quiz_format_duration($result['time_end'] - $result['time_start']);
    $passed = $result['score'] >= $result['pass_rate'];
    $grade = $passed ? t('Passed') : t('Failed');
    $passed_class = $passed ? 'quiz-passed' : 'quiz-failed';
    if (!is_numeric($result['score'])) {
      $score = t('In progress');
    }
    elseif (!$result['is_evaluated']) {
      $score = t('Not evaluated');
    }
    else {
      if (!empty($result['pass_rate']) && is_numeric($result['score'])) {
        $pre_score = '<span class = "' . $passed_class . '">';
        $post_score = ' %<br><em>' . $grade . '</em></span>';
      }
      else {
        $post_score = ' %';
      }
      $score = $pre_score . $result['score'] . $post_score;
    }
    $rows[] = array(
      'title' => l($result['title'], 'user/quiz/' . $result['result_id'] . '/userresults'),
      'time_start' => format_date($result['time_start'], 'small'),
      'time_end' => $result['time_end'] > 0 ? format_date($result['time_end'], 'small') . '<br />' . t('Duration :  @value', array(
        '@value' => $interval,
      )) : t('In Progress'),
      'score' => $score,
      'evaluated' => $result['is_evaluated'] ? t('Yes') : t('No'),
    );
  }
  if (empty($rows)) {
    return t('No @quiz results found.', array(
      '@quiz' => QUIZ_NAME,
    ));
  }
  $header = array(
    t('@quiz Title', array(
      '@quiz' => QUIZ_NAME,
    )),
    t('Started'),
    t('Finished'),
    t('Score'),
    t('Evaluated'),
  );
  $output = theme('table', $header, $rows);
  $output .= '<p><em>' . t('@quizzes that are not evaluated may have different score and grade when it is done.', array(
    '@quizzes' => QUIZ_NAME,
  )) . '</em></p>';
  return $output;
}

/**
 * Theme the user results page.
 *
 * @param $results
 *  An array of quiz information.
 * @return
 *  Themed html.
 *
 * @ingroup themeable
 */
function theme_quiz_my_results_for_quiz($results) {
  $rows = array();
  $with_passing = FALSE;
  while (list($key, $result) = each($results)) {
    $interval = _quiz_format_duration($result['time_end'] - $result['time_start']);
    $score = $result['score'] . ' %';
    $row = array(
      'time_start' => format_date($result['time_start'], 'small'),
      'duration' => $interval,
      'score' => $score,
    );
    if (!empty($result['pass_rate'])) {
      $with_passing = TRUE;
      $passed = $result['score'] >= $result['pass_rate'];
      $grade = $passed ? t('Passed') : t('Failed');
      $passed_class = $passed ? 'quiz-passed' : 'quiz-failed';
      $pre = '<span class = "' . $passed_class . '">';
      $row['passed'] = $pre . $grade . '</span>';
    }
    $row['more'] = l(t('More') . '...', 'node/' . $result['nid'] . '/myresults/' . $result['result_id']);
    $rows[] = $row;
  }
  if (empty($rows)) {
    return t('No @quiz results found.', array(
      '@quiz' => QUIZ_NAME,
    ));
  }
  $header = array(
    t('Started'),
    t('Duration'),
    t('Score'),
  );
  if ($with_passing) {
    $header[] = t('Passed');
  }
  $header[] = '';
  $output = theme('table', $header, $rows);
  return $output;
}

/**
 * Pass the correct mark to the theme so that theme authors can use an image.
 *
 * @ingroup themeable
 */
function theme_quiz_score_correct() {
  return theme('image', drupal_get_path('module', 'quiz') . '/images/correct.gif', t('correct'));
}

/**
 * Pass the incorrect mark to the theme so that theme authors can use an image.
 *
 * @ingroup themeable
 */
function theme_quiz_score_incorrect() {
  return theme('image', drupal_get_path('module', 'quiz') . '/images/incorrect.gif', t('incorrect'));
}

/**
 * Theme a progress indicator for use during a quiz.
 *
 * @param $question_number
 *  The position of the current question in the sessions' array.
 * @param $num_of_question
 *  The number of questions for this quiz as returned by quiz_get_number_of_questions().
 * @return
 *  Themed html.
 *
 * @ingroup themeable
 */
function theme_quiz_progress($question_number, $num_of_question, $jumper = FALSE, $time_limit = FALSE) {

  // Determine the percentage finished (not used, but left here for other implementations).

  //$progress = ($question_number*100)/$num_of_question;

  // Get the current question # by adding one.
  $current_question = $question_number + 1;
  if ($jumper) {
    $current_question = theme('quiz_jumper', $current_question, $num_of_question);
  }
  $output = '';
  $output .= '<div id="quiz_progress">';
  $output .= t('Question <span id="quiz-question-number">!x</span> of <span id="quiz-num-questions">@y</span>', array(
    '!x' => $current_question,
    '@y' => $num_of_question,
  ));
  $output .= '</div>' . "\n";

  // Add div to be used by jQuery countdown
  if ($time_limit) {
    $output .= '<div class="countdown"></div>';
  }
  return $output;
}
function theme_quiz_jumper($current, $num_questions) {
  $output = '<select name="quiz-jumper" class="form-select" id="quiz-jumper">';
  for ($i = 1; $i <= $num_questions; $i++) {
    $extra = $i == $current ? ' selected="selected"' : '';
    $output .= '<option value="' . $i . '"' . $extra . '>' . $i . '</option>';
  }
  $output .= '</select><span id="quiz-jumper-no-js">' . $current . '</span>';
  drupal_add_js("\n    Drupal.behaviors.quizJumper = function () {\n      \$('#quiz-jumper:not(.quizJumper-processed)').show().addClass('quizJumper-processed').change(function(){\n        \$('#edit-jump-to-question').val(\$(this).val());\n        \$('#edit-submit').trigger('click');\n      });\n      \$('#quiz-jumper-no-js:not(.quizJumper-processed)').hide().addClass('quizJumper-processed');\n    };\n  ", 'inline');
  return $output;
}

/**
 * Theme the summary page after the quiz has been completed.
 *
 * @param $quiz
 *  The quiz node object.
 * @param $questions
 *  The questions array as defined by _quiz_get_answers.
 * @param $score
 *  Array of score information as returned by quiz_calculate_score().
 * @param $summary
 *  Filtered text of the summary.
 * @return
 *  Themed html.
 *
 * @ingroup themeable
 */
function theme_quiz_take_summary($quiz, $questions, $score, $summary) {

  // Set the title here so themers can adjust.
  drupal_set_title(check_plain($quiz->title));

  // Display overall result.
  $output = '';
  if (!empty($score['possible_score'])) {
    if (!$score['is_evaluated']) {
      $msg = t('Parts of this @quiz have not been evaluated yet. The score below is not final.', array(
        '@quiz' => QUIZ_NAME,
      ));
      drupal_set_message($msg, 'warning');
    }
    $output .= '<div id="quiz_score_possible">' . t('You got %num_correct of %question_count possible points.', array(
      '%num_correct' => $score['numeric_score'],
      '%question_count' => $score['possible_score'],
    )) . '</div>' . "\n";
    $output .= '<div id="quiz_score_percent">' . t('Your score: %score %', array(
      '%score' => $score['percentage_score'],
    )) . '</div>' . "\n";
  }
  if (isset($summary['passfail'])) {
    $output .= '<div id="quiz_summary">' . $summary['passfail'] . '</div>' . "\n";
  }
  if (isset($summary['result'])) {
    $output .= '<div id="quiz_summary">' . $summary['result'] . '</div>' . "\n";
  }

  // Get the feedback for all questions. These are included here to provide maximum flexibility for themers
  if ($quiz->display_feedback) {
    $output .= drupal_get_form('quiz_report_form', $questions);
  }
  return $output;
}

/**
 * Theme the summary page for user results.
 *
 * @param $quiz
 *  The quiz node object.
 * @param $questions
 *  The questions array as defined by _quiz_get_answers.
 * @param $score
 *  Array of score information as returned by quiz_calculate_score().
 * @param $summary
 *  Filtered text of the summary.
 * @return
 *  Themed html.
 *
 * @ingroup themeable
 */
function theme_quiz_user_summary($quiz, $questions, $score, $summary) {

  // Set the title here so themers can adjust.
  drupal_set_title(check_plain($quiz->title));
  if (!$score['is_evaluated']) {
    $msg = t('Parts of this @quiz have not been evaluated yet. The score below is not final.', array(
      '@quiz' => QUIZ_NAME,
    ));
    drupal_set_message($msg, 'status');
  }

  // Display overall result.
  $output = '';
  $output .= '<div id="quiz_score_possible">' . t('You got %num_correct of %question_count possible points.', array(
    '%num_correct' => $score['numeric_score'],
    '%question_count' => $score['possible_score'],
  )) . '</div>' . "\n";
  $output .= '<div id="quiz_score_percent">' . t('Your score was: @score %', array(
    '@score' => $score['percentage_score'],
  )) . '</div>' . "\n";
  if (isset($summary['passfail'])) {
    $output .= '<div id="quiz_summary">' . $summary['passfail'] . '</div>' . "\n";
  }
  if (isset($summary['result'])) {
    $output .= '<div id="quiz_summary">' . $summary['result'] . '</div>' . "\n";
  }

  // Get the feedback for all questions.
  $output .= drupal_get_form('quiz_report_form', $questions, FALSE, TRUE);
  return $output;
}

/**
 * Theme the "no feedback" option.
 *
 * @return
 *  Themed html feedback.
 *
 * @ingroup themeable
 */
function theme_quiz_no_feedback() {
  return t('Thanks for taking the quiz!');
}

/**
 * Theme the single question node
 *
 * @param $node
 *  The question node
 * @return
 *  Themed html feedback
 */
function theme_quiz_single_question_node($node) {

  // This might seem meaningless, but it is designed this way to allow themes to add more
  // meaningful stuff here...
  return $node->body;
}

/**
 * Theme the stats on the views page
 *
 * @param $node
 *   The quiz node
 */
function theme_quiz_view_stats($node) {

  // Fetch data
  $stats = array(
    array(
      'title' => t('Questions'),
      'data' => $node->number_of_questions,
    ),
  );
  if ($node->show_attempt_stats) {
    $takes = $node->takes == 0 ? t('Unlimited') : $node->takes;
    $stats[] = array(
      'title' => t('Attempts allowed'),
      'data' => $takes,
    );
  }
  if ($node->quiz_always) {
    $stats[] = array(
      'title' => t('Available'),
      'data' => t('Always'),
    );
  }
  else {
    $stats[] = array(
      'title' => t('Opens'),
      'data' => format_date($node->quiz_open, 'small'),
    );
    $stats[] = array(
      'title' => t('Closes'),
      'data' => format_date($node->quiz_close, 'small'),
    );
  }
  if (!empty($node->pass_rate)) {
    $stats[] = array(
      'title' => t('Pass rate'),
      'data' => $node->pass_rate . ' %',
    );
  }
  if (!empty($node->time_limit)) {
    $stats[] = array(
      'title' => t('Time limit'),
      'data' => _quiz_format_duration($node->time_limit),
    );
  }
  $stats[] = array(
    'title' => t('Backwards navigation'),
    'data' => $node->backwards_navigation ? t('Allowed') : t('Forbidden'),
  );

  // Format and output the data
  $out = '<table id="quiz-view-table">' . "\n";
  foreach ($stats as $stat) {
    $out .= '<tr><td class="quiz-view-table-title"><strong>' . $stat['title'] . ':</strong></td><td class="quiz-view-table-data"><em>' . $stat['data'] . '</em></td></tr>' . "\n";
  }
  $out .= '</table>' . "\n";
  return $out;
}

Functions

Namesort descending Description
quiz_get_score_array Returns an array of score information for a quiz
quiz_get_user_results Displays all the quizzes the user has taken part in.
quiz_my_results Show results for the current quiz
quiz_report_form Form for showing feedback, and for editing the feedback if necessary...
quiz_report_form_submit Submit the report form
quiz_report_form_validate Validate the report form
quiz_update_total_score_fast Updates the total score using only one mySql query.
quiz_user_results Show result page for a given result id
theme_quiz_get_user_results Theme the user results page.
theme_quiz_jumper
theme_quiz_my_results_for_quiz Theme the user results page.
theme_quiz_no_feedback Theme the "no feedback" option.
theme_quiz_progress Theme a progress indicator for use during a quiz.
theme_quiz_score_correct Pass the correct mark to the theme so that theme authors can use an image.
theme_quiz_score_incorrect Pass the incorrect mark to the theme so that theme authors can use an image.
theme_quiz_single_question_node Theme the single question node
theme_quiz_take_summary Theme the summary page after the quiz has been completed.
theme_quiz_user_summary Theme the summary page for user results.
theme_quiz_view_stats Theme the stats on the views page
_quiz_remove_unscored_message Helper function to remove the message saying the quiz haven't been scored