You are here

quiz.admin.inc in Quiz 7.5

Administrator interface for Quiz module.

File

quiz.admin.inc
View source
<?php

/**
 * @file
 * Administrator interface for Quiz module.
 */

// QUIZ ADMIN.
// Quiz Admin Settings.

/**
 * This builds the main settings form for the quiz module.
 */
function quiz_admin_settings($form, &$form_state) {
  $form = array();
  $form['quiz_global_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Global configuration'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#description' => t("Control aspects of the Quiz module's display"),
  );
  $form['quiz_global_settings']['quiz_auto_revisioning'] = array(
    '#type' => 'checkbox',
    '#title' => t('Auto revisioning'),
    '#default_value' => variable_get('quiz_auto_revisioning', 1),
    '#description' => t('It is strongly recommended that auto revisioning is always on. It makes sure that when a question or quiz is changed a new revision is created if the current revision has been answered. If this feature is switched off result reports might be broken because a users saved answer might be connected to a wrong version of the quiz and/or question she was answering. All sorts of errors might appear.'),
  );
  $form['quiz_global_settings']['quiz_durod'] = array(
    '#type' => 'checkbox',
    '#title' => t('Delete results when a user is deleted'),
    '#default_value' => variable_get('quiz_durod', 0),
    '#description' => t('When a user is deleted delete any and all results for that user.'),
  );
  $form['quiz_global_settings']['quiz_index_questions'] = array(
    '#type' => 'checkbox',
    '#title' => t('Index questions'),
    '#default_value' => variable_get('quiz_index_questions', 1),
    '#description' => t('If you turn this off, questions will not show up in search results. Note that you will need to enable the "View quiz question outside of a quiz" permission for anonymous users, as search needs this to index the question.'),
  );
  $form['quiz_global_settings']['quiz_default_close'] = array(
    '#type' => 'textfield',
    '#title' => t('Default number of days before a @quiz is closed', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#default_value' => variable_get('quiz_default_close', 30),
    '#size' => 4,
    '#maxlength' => 4,
    '#description' => t('Supply a number of days to calculate the default close date for new quizzes.'),
  );
  $form['quiz_global_settings']['quiz_use_passfail'] = array(
    '#type' => 'checkbox',
    '#title' => t('Allow quiz creators to set a pass/fail option when creating a @quiz.', array(
      '@quiz' => strtolower(QUIZ_NAME),
    )),
    '#default_value' => variable_get('quiz_use_passfail', 1),
    '#description' => t('Check this to display the pass/fail options in the @quiz form. If you want to prohibit other quiz creators from changing the default pass/fail percentage, uncheck this option.', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['quiz_global_settings']['quiz_max_result_options'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum result options'),
    '#description' => t('Set the maximum number of result options (categorizations for scoring a quiz). Set to 0 to disable result options.'),
    '#default_value' => variable_get('quiz_max_result_options', 5),
    '#size' => 2,
    '#maxlength' => 2,
    '#required' => FALSE,
  );
  $form['quiz_global_settings']['quiz_remove_partial_quiz_record'] = array(
    '#title' => t('Remove incomplete quiz records (older than)'),
    '#description' => t('Number of days to keep incomplete quiz attempts.'),
    '#default_value' => variable_get('quiz_remove_partial_quiz_record', 0),
    '#type' => module_exists('timeperiod') ? 'timeperiod_select' : 'textfield',
    '#units' => array(
      '86400' => array(
        'max' => 30,
        'step size' => 1,
      ),
      '3600' => array(
        'max' => 24,
        'step size' => 1,
      ),
      '60' => array(
        'max' => 60,
        'step size' => 1,
      ),
    ),
  );
  $form['quiz_global_settings']['quiz_remove_invalid_quiz_record'] = array(
    '#title' => t('Remove invalid quiz records (older than)'),
    '#description' => t('Number of days to keep invalid quiz attempts.'),
    '#default_value' => variable_get('quiz_remove_invalid_quiz_record', 86400),
    '#type' => module_exists('timeperiod') ? 'timeperiod_select' : 'textfield',
    '#units' => array(
      '86400' => array(
        'max' => 30,
        'step size' => 1,
      ),
      '3600' => array(
        'max' => 24,
        'step size' => 1,
      ),
      '60' => array(
        'max' => 60,
        'step size' => 1,
      ),
    ),
  );
  $form['quiz_global_settings']['quiz_autotitle_length'] = array(
    '#type' => 'textfield',
    '#title' => t('Length of automatically set question titles'),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t("Integer between 0 and 128. If the question creator doesn't set a question title the system will make a title automatically. Here you can decide how long the autotitle can be."),
    '#default_value' => variable_get('quiz_autotitle_length', 50),
  );
  $form['quiz_global_settings']['quiz_pager_start'] = array(
    '#type' => 'textfield',
    '#title' => t('Pager start'),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('If a quiz has this many questions, a pager will be displayed instead of a select box.'),
    '#default_value' => variable_get('quiz_pager_start', 100),
  );
  $form['quiz_global_settings']['quiz_pager_siblings'] = array(
    '#type' => 'textfield',
    '#title' => t('Pager siblings'),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('Number of siblings to show.'),
    '#default_value' => variable_get('quiz_pager_siblings', 5),
  );
  $form['quiz_global_settings']['quiz_time_limit_buffer'] = array(
    '#type' => 'textfield',
    '#title' => t('Time limit buffer'),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('How many seconds after the time limit runs out to allow answers.'),
    '#default_value' => variable_get('quiz_time_limit_buffer', 5),
    '#element_validate' => array(
      'element_validate_integer',
    ),
  );

  // Review options.
  $review_options = quiz_get_feedback_options();
  $form['quiz_global_settings']['quiz_admin_review_options']['#title'] = t('Administrator review options');
  $form['quiz_global_settings']['quiz_admin_review_options']['#type'] = 'fieldset';
  $form['quiz_global_settings']['quiz_admin_review_options']['#description'] = t('Control what feedback types quiz administrators will see when viewing results for other users.');
  foreach (quiz_get_feedback_times() as $key => $when) {
    $form['quiz_global_settings']['quiz_admin_review_options']["quiz_admin_review_options_{$key}"] = array(
      '#title' => $when['name'],
      '#type' => 'checkboxes',
      '#options' => $review_options,
      '#default_value' => variable_get("quiz_admin_review_options_{$key}", array()),
    );
  }
  $target = array(
    'attributes' => array(
      'target' => '_blank',
    ),
  );
  $links = array(
    '!jquery_countdown' => l(t('JQuery Countdown'), 'http://drupal.org/project/jquery_countdown', $target),
  );
  $form['quiz_addons'] = array(
    '#type' => 'fieldset',
    '#title' => t('Addons configuration'),
    '#description' => t('Quiz has built in integration with some other modules. Disabled checkboxes indicates that modules are not enabled.', $links),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['quiz_addons']['quiz_has_timer'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display timer'),
    '#default_value' => variable_get('quiz_has_timer', 0),
    '#description' => t("!jquery_countdown is an <strong>optional</strong> module for Quiz. It is used to display a timer when taking a quiz. Without this timer, the user will not know how much time they have left to complete the Quiz", $links),
    '#disabled' => !function_exists('jquery_countdown_add'),
  );
  $form['quiz_look_feel'] = array(
    '#type' => 'fieldset',
    '#title' => t('Look and feel'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#description' => t("Control aspects of the Quiz module's display"),
  );
  $form['quiz_look_feel']['quiz_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Display name'),
    '#default_value' => QUIZ_NAME,
    '#description' => t('Change the name of the quiz type. Do you call it <em>test</em> or <em>assessment</em> instead? Change the display name of the module to something else. By default, it is called <em>Quiz</em>.'),
    '#required' => TRUE,
  );
  $form['#validate'][] = 'quiz_settings_form_validate';
  $form['#submit'][] = 'quiz_settings_form_submit';
  return system_settings_form($form);
}

/**
 * Validation of the Form Settings form.
 *
 * Checks the values for the form administration form for quiz settings.
 */
function quiz_settings_form_validate($form, &$form_state) {
  if (!_quiz_is_int($form_state['values']['quiz_default_close'])) {
    form_set_error('quiz_default_close', t('The default number of days before a quiz is closed must be a number greater than 0.'));
  }
  if (!_quiz_is_int($form_state['values']['quiz_autotitle_length'], 0, 128)) {
    form_set_error('quiz_autotitle_length', t('The autotitle length value must be an integer between 0 and 128.'));
  }
  if (!_quiz_is_int($form_state['values']['quiz_max_result_options'], 0, 100)) {
    form_set_error('quiz_max_result_options', t('The number of resultoptions must be an integer between 0 and 100.'));
  }
  if (!_quiz_is_plain($form_state['values']['quiz_name'])) {
    form_set_error('quiz_name', t('The quiz name must be plain text.'));
  }

  /* if (!_quiz_is_plain($form_state['values']['quiz_action_type']))
     form_set_error('quiz_action_type', t('The action type must be plain text.')); */
}

/**
 * Submit the admin settings form.
 */
function quiz_settings_form_submit($form, &$form_state) {
  if (QUIZ_NAME != $form_state['values']['quiz_name']) {
    variable_set('quiz_name', $form_state['values']['quiz_name']);
    define(QUIZ_NAME, $form_state['values']['quiz_name']);
    menu_rebuild();
  }
}

/**
 * Renders the quiz node form for the admin pages.
 *
 * This form is used to configure default values for the quiz node form.
 */
function quiz_admin_node_form($form, &$form_state) {

  // Create a dummy node to use as input for quiz_form.
  $dummy_node = new stdClass();

  // Def_uid is the uid of the default user holding the default values for the
  // node form(no real user with this uid exists).
  $settings = quiz_get_defaults();
  foreach ($settings as $key => $value) {
    if (!isset($dummy_node->{$key})) {
      $dummy_node->{$key} = $value;
    }
  }
  $form = quiz_form($dummy_node, $form_state);
  $form['direction'] = array(
    '#markup' => t('Here you can change the default @quiz settings for new users.', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#weight' => -10,
  );

  // Unset values we can't or won't let the user edit default values for.
  unset($form['#quiz_check_revision_access'], $form['title'], $form['body_field'], $form['taking']['addons'], $form['quiz_availability']['quiz_open'], $form['quiz_availability']['quiz_close'], $form['resultoptions'], $form['number_of_random_questions'], $form['remember_global']);
  $form['remember_settings']['#type'] = 'value';
  $form['remember_settings']['#default_value'] = TRUE;
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
    '#submit' => array(
      'quiz_admin_node_form_submit',
    ),
  );
  return $form;
}

/**
 * Validation function for the quiz_admin_node_form form.
 */
function quiz_admin_node_form_validate($form, &$form_state) {

  // Create dummy node for quiz_validate.
  $dummy_node = new stdClass();
  foreach ($form_state['values'] as $key => $value) {
    $dummy_node->{$key} = $value;
  }
  $dummy_node->resultoptions = array();

  // We use quiz_validate to validate the default values.
  quiz_validate($dummy_node);
}

/**
 * Submit function for quiz_admin_node_form.
 *
 * The default values are saved as the user settings for the "default user". The
 * default user is created when quiz is installed. He has a unique uid, but
 * doesn't exist as a real user.
 *
 * Why?
 * Default user settings can be loaded and saved using the same code and
 * database tables as any other user settings, making the code a lot easier to
 * maintain.
 * Ref: http://en.wikipedia.org/wiki/Don%27t_repeat_yourself
 */
function quiz_admin_node_form_submit($form, &$form_state) {

  // We add the uid for the "default user"
  // Generate the node object:
  $node = (object) $form_state['values'];
  $node->uid = 0;
  $node->nid = 0;
  $node->vid = 0;
  quiz_update_defaults($node);
  $form_state['node'] = $node;
}

// QUIZ RESULTS ADMIN.

/**
 * Quiz result report page for the quiz admin section.
 *
 * @param $quiz
 *   The quiz node.
 * @param $result_id
 *   The result id.
 */
function quiz_admin_results($quiz, $quiz_result) {

  // Preserve "Results" tab.
  $item = menu_get_item("node/{$quiz->nid}/quiz");
  menu_set_item(NULL, $item);
  return entity_ui_get_form('quiz_result', $quiz_result);
}

// MANAGE QUESTIONS.

/**
 * Creates a form for quiz questions.
 *
 * Handles the manage questions tab.
 *
 * @param $node
 *   The quiz node we are managing questions for.
 * @return
 *   String containing the form.
 */
function quiz_questions_page($node) {
  if ($node->randomization < 3) {
    $mq_form = drupal_get_form('quiz_questions_form', $node);
    $manage_questions = drupal_render($mq_form);
    $question_bank = views_get_view('quiz_question_bank')
      ->preview();

    // Insert into vert tabs.
    $form['vert_tabs'] = array(
      '#type' => 'vertical_tabs',
      '#weight' => 0,
    );
    $form['vert_tabs']['question_admin'] = array(
      '#type' => 'fieldset',
      '#title' => t('Manage questions'),
      '#value' => $manage_questions,
    );
    $form['vert_tabs']['global_questions'] = array(
      '#type' => 'fieldset',
      '#title' => t('Question bank'),
      '#value' => $question_bank,
    );
  }
  else {
    $form = drupal_get_form('quiz_categorized_form', $node);
  }
  $variables = array(
    'node' => $node,
    'form' => $form,
  );
  return theme('quiz_questions_page', $variables);
}

/**
 * Form for managing what questions should be added to a quiz with categorized
 * random questions.
 *
 * @param array $form_state
 *  The form state array.
 * @param object $quiz
 *  The quiz node.
 */
function quiz_categorized_form($form, $form_state, $quiz) {
  $form = array();
  _quiz_categorized_existing_terms_form($form, $form_state, $quiz);
  _quiz_categorized_new_term_form($form, $form_state, $quiz);
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $quiz->nid,
  );
  $form['vid'] = array(
    '#type' => 'value',
    '#value' => $quiz->vid,
  );
  $form['tid'] = array(
    '#type' => 'value',
    '#value' => NULL,
  );

  // Give the user the option to create a new revision of the quiz.
  _quiz_add_revision_checkbox($form, $quiz);

  // Timestamp is needed to avoid multiple users editing the same quiz at the
  // same time.
  $form['timestamp'] = array(
    '#type' => 'hidden',
    '#default_value' => REQUEST_TIME,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form['#tree'] = TRUE;
  return $form;
}
function _quiz_categorized_existing_terms_form(&$form, $form_state, $quiz) {
  $terms = _quiz_get_terms($quiz->vid);
  if ($terms) {
    if (empty($form_state['input']) && !_quiz_build_categorized_question_list($quiz)) {
      drupal_set_message(t('There are not enough questions in the requested categories.'), 'error');
    }
  }
  foreach ($terms as $term) {
    $form[$term->tid]['name'] = array(
      '#markup' => check_plain($term->name),
    );
    $form[$term->tid]['number'] = array(
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => $term->number,
    );
    $form[$term->tid]['max_score'] = array(
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => $term->max_score,
    );
    $form[$term->tid]['remove'] = array(
      '#type' => 'checkbox',
      '#default_value' => 0,
    );
    $form[$term->tid]['weight'] = array(
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => $term->weight,
      '#attributes' => array(
        'class' => array(
          'term-weight',
        ),
      ),
    );
  }
}

/**
 * Form for adding new terms to a quiz.
 *
 * @see quiz_categorized_form
 */
function _quiz_categorized_new_term_form(&$form, $form_state, $quiz) {
  $form['new'] = array(
    '#type' => 'fieldset',
    '#title' => t('Add category'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
    '#tree' => FALSE,
  );
  $form['new']['term'] = array(
    '#type' => 'textfield',
    '#title' => t('Category'),
    '#description' => t('Type in the name of the term you would like to add questions from.'),
    '#autocomplete_path' => "node/{$quiz->nid}/questions/term_ahah",
    '#field_suffix' => '<a id="browse-for-term" href="javascript:void(0)">' . t('browse') . '</a>',
  );
  $form['new']['number'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of questions'),
    '#description' => t('How many questions would you like to draw from this term?'),
  );
  $form['new']['max_score'] = array(
    '#type' => 'textfield',
    '#title' => t('Max score for each question'),
    '#description' => t('The number of points a user will be awarded for each question he gets correct.'),
    '#default_value' => 1,
  );
}

/**
 * Validate the categorized form.
 */
function quiz_categorized_form_validate($form, &$form_state) {
  if ($form_state['build_info']['args'][0]->changed > $form_state['values']['timestamp']) {
    form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.'));
  }
  if (!empty($form_state['values']['term'])) {
    $tid = _quiz_get_id_from_string($form_state['values']['term']);
    if ($tid === FALSE) {
      $terms = _quiz_search_terms($form_state['values']['term']);
      $num_terms = count($terms);
      if ($num_terms == 1) {
        $tid = key($terms);
      }
      elseif ($num_terms > 1) {
        form_set_error('term', t('You need to be more specific, or use the autocomplete feature. The term name you entered matches several terms: %terms', array(
          '%terms' => implode(', ', $terms),
        )));
      }
      elseif ($num_terms == 0) {
        form_set_error('term', t("The term name you entered doesn't match any registered question terms."));
      }
    }
    if (in_array($tid, array_keys($form))) {
      form_set_error('term', t('The category you are trying to add has already been added to this quiz.'));
    }
    else {
      form_set_value($form['tid'], $tid, $form_state);
    }
    if (!_quiz_is_int($form_state['values']['number'])) {
      form_set_error('number', t('The number of questions needs to be a positive integer'));
    }
    if (!_quiz_is_int($form_state['values']['max_score'], 0)) {
      form_set_error('max_score', t('The max score needs to be a positive integer or 0'));
    }
  }
}

/**
 * Submit the categorized form.
 */
function quiz_categorized_form_submit($form, $form_state) {
  $quiz = node_load($form_state['values']['nid'], $form_state['values']['vid']);
  $quiz->number_of_random_questions = 0;

  // Update the refresh latest quizzes table so that we know what the users
  // latest quizzes are.
  if (variable_get('quiz_auto_revisioning', 1)) {
    $is_new_revision = quiz_has_been_answered($quiz);
  }
  else {
    $is_new_revision = (bool) $form_state['values']['new_revision'];
  }
  if (!empty($form_state['values']['tid'])) {
    $quiz->number_of_random_questions += _quiz_categorized_add_term($form, $form_state);
  }
  $quiz->number_of_random_questions += _quiz_categorized_update_terms($form, $form_state);
  if ($is_new_revision) {
    $quiz->revision = 1;
  }

  // We save the node to update its timestamp and let other modules react to the
  // update. We also do this in case a new revision is required...
  node_save($quiz);
}

/**
 * Update the categoriez belonging to a quiz with categorized random questions.
 *
 * Helper function for quiz_categorized_form_submit.
 */
function _quiz_categorized_update_terms(&$form, &$form_state) {
  $ids = array(
    'weight',
    'max_score',
    'number',
  );
  $changed = array();
  $removed = array();
  $num_questions = 0;
  foreach ($form_state['values'] as $key => $existing) {
    if (!is_numeric($key)) {
      continue;
    }
    if (!$existing['remove']) {
      $num_questions += $existing['number'];
    }
    foreach ($ids as $id) {
      if ($existing[$id] != $form[$key][$id]['#default_value'] && !$existing['remove']) {
        $existing['nid'] = $form_state['values']['nid'];
        $existing['vid'] = $form_state['values']['vid'];
        $existing['tid'] = $key;
        if (empty($existing['weight'])) {
          $existing['weight'] = 1;
        }
        $changed[] = $form[$key]['name']['#markup'];
        drupal_write_record('quiz_terms', $existing, array(
          'vid',
          'tid',
        ));
        break;
      }
      elseif ($existing['remove']) {
        db_delete('quiz_terms')
          ->condition('tid', $key)
          ->condition('vid', $form_state['values']['vid'])
          ->execute();
        $removed[] = $form[$key]['name']['#markup'];
        break;
      }
    }
  }
  if (!empty($changed)) {
    drupal_set_message(t('Updates were made for the following terms: %terms', array(
      '%terms' => implode(', ', $changed),
    )));
  }
  if (!empty($removed)) {
    drupal_set_message(t('The following terms were removed: %terms', array(
      '%terms' => implode(', ', $removed),
    )));
  }
  return $num_questions;
}

/**
 * Adds a term to a categorized quiz.
 *
 * This is a helper function for the submit function.
 */
function _quiz_categorized_add_term($form, $form_state) {
  drupal_set_message(t('The term was added'));

  // Needs to be set to avoid error-message from db:
  $form_state['values']['weight'] = 0;
  drupal_write_record('quiz_terms', $form_state['values']);
  return $form_state['values']['number'];
}

/**
 * Searches for an id in the end of a string.
 *
 * Id should be written like "(id:23)".
 *
 * @param string $string
 *   The string where we will search for an id.
 * @return int
 *   The matched integer.
 */
function _quiz_get_id_from_string($string) {
  $matches = array();
  preg_match('/\\(id:(\\d+)\\)$/', $string, $matches);
  return isset($matches[1]) ? (int) $matches[1] : FALSE;
}

/**
 * Ahah function for finding terms...
 *
 * @param string $start
 *   The start of the string we are looking for.
 */
function quiz_categorized_term_ahah($start) {
  $terms = _quiz_search_terms($start, $start == '*');
  $to_json = array();
  foreach ($terms as $key => $value) {
    $to_json["{$value} (id:{$key})"] = $value;
  }
  drupal_json_output($to_json);
}

/**
 * Helper function for finding terms...
 *
 * @param string $start
 *   The start of the string we are looking for.
 */
function _quiz_search_terms($start, $all = FALSE) {
  $terms = array();
  $sql_args = array_keys(_quiz_get_vocabularies());
  if (empty($sql_args)) {
    return $terms;
  }
  $query = db_select('taxonomy_term_data', 't')
    ->fields('t', array(
    'name',
    'tid',
  ))
    ->condition('t.vid', $sql_args, 'IN');
  if (!$all) {
    $query
      ->condition('t.name', '%' . $start . '%', 'LIKE');
  }
  $res = $query
    ->execute();

  // @todo Don't user db_fetch_object.
  while ($res_o = $res
    ->fetch()) {
    $terms[$res_o->tid] = $res_o->name;
  }
  return $terms;
}

/**
 * Handles "manage questions" tab.
 *
 * Displays form which allows questions to be assigned to the given quiz.
 *
 * This function is not used if the question assignment type "categorized random
 * questions" is chosen.
 *
 * @param $form_state
 *   The form state variable
 * @param $quiz
 *   The quiz node.
 *
 * @return
 *   HTML output to create page.
 */
function quiz_questions_form($form, $form_state, $quiz) {
  $types = quiz_get_question_types();
  _quiz_add_fields_for_creating_questions($form, $types, $quiz);

  // Display questions in this quiz.
  $form['question_list'] = array(
    '#type' => 'fieldset',
    '#title' => t('Questions in this @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#theme' => 'question_selection_table',
    '#collapsible' => TRUE,
    '#attributes' => array(
      'id' => 'mq-fieldset',
    ),
    'question_status' => array(
      '#tree' => TRUE,
    ),
  );

  // Add randomization settings if this quiz allows randomized questions.
  _quiz_add_fields_for_random_quiz($form, $quiz);
  $include_random = $quiz->randomization == 2;

  // @todo deal with $include_random.
  $questions = quiz_get_questions($quiz->nid, $quiz->vid);
  if (empty($questions)) {
    $form['question_list']['no_questions'] = array(
      '#markup' => '<div id = "no-questions">' . t('There are currently no questions in this @quiz. Assign existing questions by using the question browser below. You can also use the links above to create new questions.', array(
        '@quiz' => QUIZ_NAME,
      )) . '</div>',
    );
  }

  // We add the questions to the form array.
  _quiz_add_questions_to_form($form, $questions, $quiz, $types);

  // Show the number of questions in the table header.
  $always_count = isset($form['question_list']['titles']) ? count($form['question_list']['titles']) : 0;
  $form['question_list']['#title'] .= ' (' . $always_count . ')';

  // Give the user the option to create a new revision of the quiz.
  _quiz_add_revision_checkbox($form, $quiz);

  // Timestamp is needed to avoid multiple users editing the same quiz at the
  // same time.
  $form['timestamp'] = array(
    '#type' => 'hidden',
    '#default_value' => REQUEST_TIME,
  );
  $form['actions']['#type'] = 'actions';
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
    '#submit' => array(
      'quiz_questions_form_submit',
    ),
  );
  return $form;
}

/**
 * Fields for creating new questions are added to the quiz_questions_form.
 *
 * @param $form
 *   FAPI form(array).
 * @param $types
 *   All the question types(array).
 * @param $quiz
 *   The quiz node.
 */
function _quiz_add_fields_for_creating_questions(&$form, &$types, &$quiz) {

  // Display links to create other questions.
  $form['additional_questions'] = array(
    '#type' => 'fieldset',
    '#title' => t('Create new question'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $url_query = drupal_get_destination();
  $url_query['quiz_nid'] = $quiz->nid;
  $url_query['quiz_vid'] = $quiz->vid;
  $create_question = FALSE;
  foreach ($types as $type => $info) {
    $url_type = str_replace('_', '-', $type);
    $options = array(
      'attributes' => array(
        'title' => t('Create @name', array(
          '@name' => $info['name'],
        )),
      ),
      'query' => $url_query,
    );
    $access = node_access('create', $type);
    if ($access) {
      $create_question = TRUE;
    }
    $form['additional_questions'][$type] = array(
      '#markup' => '<div class="add-questions">' . l($info['name'], "node/add/{$url_type}", $options) . '</div>',
      '#access' => $access,
    );
  }
  if (!$create_question) {
    $form['additional_questions']['create'] = array(
      '#type' => 'markup',
      '#markup' => t('You have not enabled any question type module or no has permission been given to create any question.'),
    );
  }
}

/**
 * Add fields for random quiz to the quiz_questions_form.
 *
 * @param $form
 *   FAPI form array.
 * @param $quiz
 *   The quiz node(object).
 */
function _quiz_add_fields_for_random_quiz(&$form, $quiz) {
  if ($quiz->randomization != 2) {
    return;
  }
  $form['question_list']['random_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Settings for random questions'),
    '#collapsible' => TRUE,
  );
  $form['question_list']['random_settings']['num_random_questions'] = array(
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#weight' => -5,
    '#title' => t('Number of random questions'),
    '#description' => t('The number of questions to be randomly selected each time someone takes this quiz'),
    '#default_value' => isset($quiz->number_of_random_questions) ? $quiz->number_of_random_questions : 10,
  );
  $form['question_list']['random_settings']['max_score_for_random'] = array(
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#weight' => -5,
    '#title' => t('Max score for each random question'),
    '#default_value' => isset($quiz->max_score_for_random) ? $quiz->max_score_for_random : 1,
  );
}

/**
 * Adds the questions in the $questions array to the form.
 *
 * @param $form
 *   FAPI form(array).
 * @param $questions
 *   The questions to be added to the question list(array).
 * @param $quiz
 *   The quiz node(object).
 * @param $question_types
 *   array of all available question types.
 */
function _quiz_add_questions_to_form(&$form, &$questions, &$quiz, &$question_types) {
  $form['question_list']['weights'] = array(
    '#tree' => TRUE,
  );
  $form['question_list']['qnr_ids'] = array(
    '#tree' => TRUE,
  );
  $form['question_list']['qnr_pids'] = array(
    '#tree' => TRUE,
  );
  $form['question_list']['max_scores'] = array(
    '#tree' => TRUE,
  );
  $form['question_list']['auto_update_max_scores'] = array(
    '#tree' => TRUE,
  );
  $form['question_list']['stayers'] = array(
    '#tree' => TRUE,
  );
  $form['question_list']['revision'] = array(
    '#tree' => TRUE,
  );
  if ($quiz->randomization == 2) {
    $form['question_list']['compulsories'] = array(
      '#tree' => TRUE,
    );
  }
  foreach ($questions as $question) {

    // @todo replace entire form with usage of question instance
    $question_node = node_load($question->nid, $question->vid);
    $instance = _quiz_question_get_instance($question_node);
    $fieldset = 'question_list';
    $id = $question->nid . '-' . $question->vid;
    $form[$fieldset]['weights'][$id] = array(
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => isset($question->weight) ? $question->weight : 0,
    );
    $form[$fieldset]['qnr_pids'][$id] = array(
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => $question->qnr_pid,
    );
    $form[$fieldset]['qnr_ids'][$id] = array(
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => $question->qnr_id,
    );

    // Quiz directions don't have scoring...
    $form[$fieldset]['max_scores'][$id] = array(
      '#type' => $instance
        ->isGraded() ? 'textfield' : 'hidden',
      '#size' => 2,
      '#disabled' => isset($question->auto_update_max_score) ? $question->auto_update_max_score : FALSE,
      '#default_value' => isset($question->max_score) ? $question->max_score : 0,
      '#states' => array(
        'disabled' => array(
          "#edit-auto-update-max-scores-{$id}" => array(
            'checked' => TRUE,
          ),
        ),
      ),
    );
    $form[$fieldset]['auto_update_max_scores'][$id] = array(
      '#type' => $instance
        ->isGraded() ? 'checkbox' : 'hidden',
      '#default_value' => isset($question->auto_update_max_score) ? $question->auto_update_max_score : 0,
    );

    // Add checkboxes to remove questions in js disabled browsers...
    $form[$fieldset]['stayers'][$id] = array(
      '#type' => 'checkbox',
      '#default_value' => 0,
      '#attributes' => array(
        'class' => array(
          'q-staying',
        ),
      ),
    );

    // Add checkboxes to mark compulsory questions for randomized quizzes.
    if ($quiz->randomization == 2) {
      $form[$fieldset]['compulsories'][$id] = array(
        '#type' => 'checkbox',
        '#default_value' => isset($question->question_status) ? $question->question_status == QUIZ_QUESTION_ALWAYS ? 1 : 0 : 0,
        '#attributes' => array(
          'class' => array(
            'q-compulsory',
          ),
        ),
      );
    }
    if (user_access('view quiz question outside of a quiz')) {
      $link_options = array(
        'attributes' => array(
          'class' => array(
            'handle-changes',
          ),
        ),
      );
      $question_titles = l($question->title, 'node/' . $question->nid, $link_options);
    }
    else {
      $question_titles = check_plain($question->title);
    }
    $form[$fieldset]['titles'][$id] = array(
      '#markup' => $question_titles,
    );
    $form[$fieldset]['types'][$id] = array(
      '#markup' => $question_types[$question->type]['name'],
      '#question_type' => $question->type,
    );
    $form[$fieldset]['view_links'][$id] = array(
      '#markup' => l(t('Edit'), 'node/' . $question->nid . '/edit', array(
        'query' => drupal_get_destination(),
        'attributes' => array(
          'class' => array(
            'handle-changes',
          ),
        ),
      )),
      '#access' => node_access('update', node_load($question->nid, $question->vid)),
    );

    // For js enabled browsers questions are removed by pressing a remove link.
    $form[$fieldset]['remove_links'][$id] = array(
      '#markup' => '<a href="#" class="rem-link">' . t('Remove') . '</a>',
    );

    // Add a checkbox to update to the latest revision of the question.
    if ($question->vid == $question->latest_vid) {
      $update_cell = array(
        '#markup' => t('<em>Up to date</em>'),
      );
    }
    else {
      $update_cell = array(
        '#type' => 'checkbox',
        '#title' => l(t('Latest'), 'node/' . $question->nid . '/revisions/' . $question->latest_vid . '/view') . ' of ' . l(t('revisions'), 'node/' . $question->nid . '/revisions'),
      );
    }
    $form[$fieldset]['revision'][$id] = $update_cell;
  }
}

/**
 * Adds checkbox for creating new revision. Checks it by default if answers
 * exists.
 *
 * @param $form
 *   FAPI form(array).
 *
 * @param $quiz
 *   Quiz node(object).
 */
function _quiz_add_revision_checkbox(&$form, &$quiz) {

  // Recommend and preselect to create the quiz as a new revision if it already
  // has been answered.
  if (quiz_has_been_answered($quiz)) {
    $rev_default = TRUE;
    $rev_description = t('This quiz has been answered. To maintain correctness of existing answers and reports, changes should be saved as a new revision.');
  }
  else {
    $rev_default = in_array('revision', variable_get('node_options_quiz', array()));
    $rev_description = t('Allow question status changes to create a new revision of the quiz?');
  }
  if (user_access('manual quiz revisioning') && !variable_get('quiz_auto_revisioning', 1)) {
    $form['new_revision'] = array(
      '#type' => 'checkbox',
      '#default_value' => $rev_default,
      '#title' => t('New revision'),
      '#description' => $rev_description,
    );
  }
  else {
    $form['new_revision'] = array(
      '#type' => 'value',
      '#value' => $rev_default,
    );
  }
}

/**
 * Validate that the supplied questions are real.
 */
function quiz_questions_form_validate($form, $form_state) {
  if ($form_state['build_info']['args'][0]->changed > $form_state['values']['timestamp']) {
    form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.'));
  }
  $already_checked = array();
  $weight_map = isset($form_state['values']['weights']) ? $form_state['values']['weights'] : array();

  // Make sure the number of random questions is a positive number.
  if (isset($form_state['values']['num_random_questions']) && !_quiz_is_int($form_state['values']['num_random_questions'], 0)) {
    form_set_error('num_random_questions', 'The number of random questions needs to be a positive number');
  }

  // Make sure the max score for random questions is a positive number.
  if (isset($form_state['values']['max_score_for_random']) && !_quiz_is_int($form_state['values']['max_score_for_random'], 0)) {
    form_set_error('max_score_for_random', 'The max score for random questions needs to be a positive number');
  }
  if (empty($weight_map)) {
    form_set_error('none', 'No questions were included.');
    return;
  }
  $question_types = array_keys(quiz_get_question_types());
  foreach ($weight_map as $id => $weight) {
    list($nid, $vid) = explode('-', $id, 2);

    // If a node isn't one of the questionstypes we remove it from the question
    // list.
    $has_questions = (bool) db_select('node', 'n')
      ->fields('n', array(
      'nid',
    ))
      ->condition('type', $question_types, 'IN')
      ->condition('n.nid', $nid)
      ->execute()
      ->fetchField();
    if (!$has_questions) {
      form_set_error('none', 'One of the supplied questions was invalid. It has been removed from the quiz.');
      unset($form_state['values']['weights'][$id]);
    }
    elseif (in_array($nid, $already_checked)) {
      form_set_error('none', 'A duplicate question has been removed. You can only ask a question once per quiz.');
      unset($form_state['values']['weights'][$id]);
    }
    else {
      $already_checked[] = $nid;
    }
  }

  // We make sure max score is a positive number.
  $max_scores = $form_state['values']['max_scores'];
  foreach ($max_scores as $id => $max_score) {
    if (!_quiz_is_int($max_score, 0)) {
      form_set_error("max_scores][{$id}", t('Max score needs to be a positive number'));
    }
  }
}

/**
 * Update a quiz set of items with new weights and membership.
 *
 * @param $quiz
 *   The quiz node.
 * @param $weight_map
 *   Weights for each question(determines the order in which the question will
 *   be taken by the quiz taker).
 * @param $max_scores
 *   Array of max scores for each question.
 * @param $is_new_revision
 *   Array of boolean values determining if the question is to be updated to the
 *   newest revision.
 * @param $refreshes
 *   True if we are creating a new revision of the quiz.
 * @param $stayers
 *   Questions added to the quiz.
 * @param $compulsories
 *   Array of boolean values determining if the question is compulsory or not.
 *
 * @return array set of questions after updating
 */
function _quiz_update_items($quiz, $weight_map, $max_scores, $auto_update_max_scores, $is_new_revision, $refreshes, $stayers, $qnr_ids, $qnr_pids, $compulsories = NULL) {
  $questions = array();
  foreach ($weight_map as $id => $weight) {
    if ($stayers[$id]) {
      continue;
    }
    list($nid, $vid) = explode('-', $id, 2);
    $nid = (int) $nid;
    $vid = (int) $vid;
    $question = new stdClass();
    $question->child_nid = $nid;
    if (!empty($refreshes[$id])) {

      // User marked to update question to latest revision.
      $question_node = node_load($nid);
      $question->child_vid = $question_node->vid;
    }
    else {

      // Use the provided nid/vid.
      $question->child_vid = $vid;
    }
    if (isset($compulsories)) {
      if ($compulsories[$id] == 1) {
        $question->question_status = QUIZ_QUESTION_ALWAYS;
      }
      else {
        $question->question_status = QUIZ_QUESTION_RANDOM;
        $max_scores[$id] = $quiz->max_score_for_random;
      }
    }
    else {
      $question->question_status = QUIZ_QUESTION_ALWAYS;
    }
    $question->weight = $weight;
    $question->max_score = $max_scores[$id];
    $question->auto_update_max_score = $auto_update_max_scores[$id];
    $question->qnr_pid = $qnr_pids[$id] > 0 ? $qnr_pids[$id] : NULL;
    $question->qnr_id = $qnr_ids[$id] > 0 ? $qnr_ids[$id] : NULL;

    // Add item as an object in the questions array.
    $questions[] = $question;
  }

  // Save questions.
  quiz_set_questions($quiz, $questions, $is_new_revision);
  return $questions;
}

/**
 * Submit function for quiz_questions.
 *
 * Updates from the "manage questions" tab.
 */
function quiz_questions_form_submit($form, &$form_state) {

  // Load the quiz node.
  $quiz = $form_state['build_info']['args'][0];

  // Update the refresh latest quizzes table so that we know what the users
  // latest quizzes are.
  if (variable_get('quiz_auto_revisioning', 1)) {
    $is_new_revision = quiz_has_been_answered($quiz);
  }
  else {
    $is_new_revision = !empty($form_state['values']['new_revision']);
  }
  _quiz_question_browser_submit($form, $form_state);
  $weight_map = $form_state['values']['weights'];
  $qnr_pids_map = $form_state['values']['qnr_pids'];
  $qnr_ids_map = $form_state['values']['qnr_ids'];
  $max_scores = $form_state['values']['max_scores'];
  $auto_update_max_scores = $form_state['values']['auto_update_max_scores'];
  $refreshes = isset($form_state['values']['revision']) ? $form_state['values']['revision'] : NULL;
  $stayers = $form_state['values']['stayers'];
  $compulsories = isset($form_state['values']['compulsories']) ? $form_state['values']['compulsories'] : NULL;
  $num_random = isset($form_state['values']['num_random_questions']) ? $form_state['values']['num_random_questions'] : 0;
  $quiz->max_score_for_random = isset($form_state['values']['max_score_for_random']) ? $form_state['values']['max_score_for_random'] : 1;

  // Store what questions belong to the quiz.
  $questions = _quiz_update_items($quiz, $weight_map, $max_scores, $auto_update_max_scores, $is_new_revision, $refreshes, $stayers, $qnr_ids_map, $qnr_pids_map, $compulsories);

  // If using random questions and no term ID is specified, make sure we have
  // enough.
  $assigned_random = 0;
  foreach ($questions as $question) {
    if ($question->question_status == QUIZ_QUESTION_RANDOM) {
      ++$assigned_random;
    }
  }

  // Adjust number of random questions downward to match number of selected
  // questions.
  if ($num_random > $assigned_random) {
    $num_random = $assigned_random;
    drupal_set_message(t('The number of random questions for this @quiz have been lowered to %anum to match the number of questions you assigned.', array(
      '@quiz' => QUIZ_NAME,
      '%anum' => $assigned_random,
    ), array(
      'langcode' => 'warning',
    )));
  }
  if ($quiz->type == 'quiz') {

    // Update the quiz node properties.
    $success = db_update('quiz_node_properties')
      ->fields(array(
      'number_of_random_questions' => $num_random ? $num_random : 0,
      'max_score_for_random' => $quiz->max_score_for_random,
    ))
      ->condition('vid', $quiz->vid)
      ->condition('nid', $quiz->nid)
      ->execute();

    // Get sum of max_score.
    $query = db_select('quiz_node_relationship', 'qnr');
    $query
      ->addExpression('SUM(max_score)', 'sum');
    $query
      ->condition('parent_vid', $quiz->vid);
    $query
      ->condition('question_status', QUIZ_QUESTION_ALWAYS);
    $score = $query
      ->execute()
      ->fetchAssoc();
    $success2 = db_update('quiz_node_properties')
      ->expression('max_score', 'max_score_for_random * number_of_random_questions + :sum', array(
      ':sum' => (int) $score['sum'],
    ))
      ->condition('vid', $quiz->vid)
      ->execute();
  }
  if (isset($success) && isset($success2)) {
    entity_get_controller('node')
      ->resetCache(array(
      $quiz->nid,
    ));
    drupal_set_message(t('Questions updated successfully.'));
  }
  else {
    drupal_set_message(t('There was an error updating the @quiz.', array(
      '@quiz' => QUIZ_NAME,
    )), 'error');
  }
}

/**
 * Takes care of the browser part of the submitted form values.
 *
 * This function changes the form_state to reflect questions added via the
 * browser. (Especially if js is disabled)
 *
 * @param $form
 *   FAPI form(array)
 * @param $form_state
 *   FAPI form_state(array)
 */
function _quiz_question_browser_submit($form, &$form_state) {

  // Find the biggest weight:
  $next_weight = max($form_state['values']['weights']);

  // If a question is chosen in the browser, add it to the question list if it
  // isn't already there.
  if (isset($form_state['values']['browser']['table']['titles'])) {
    foreach ($form_state['values']['browser']['table']['titles'] as $id) {
      if ($id !== 0) {
        $matches = array();
        preg_match('/([0-9]+)-([0-9]+)/', $id, $matches);
        $nid = $matches[1];
        $vid = $matches[2];
        $form_state['values']['weights'][$id] = ++$next_weight;
        $form_state['values']['max_scores'][$id] = quiz_question_get_max_score($nid, $vid);
        $form_state['values']['stayers'][$id] = 1;
      }
    }
  }
}

// HELPER FUNCTIONS.

/**
 * Helper function for theme_question_selection_table.
 *
 * TODO: DELETE
 *
 * @see quiz_questions_form()
 * @see theme_question_selection_table()
 *
 * @param $sub_form
 *   Form definition array for a filtered questions list
 * @param $id
 *   Identifier used in $sub_form
 *
 * @return table row
 *   Array defining a table row
 */
function _quiz_get_question_row($sub_form, $id) {
  $question_types = quiz_get_question_types();
  $type = $sub_form['types'][$id]['#markup'];
  $action = theme('item_list', array(
    'items' => array(
      drupal_render($sub_form['view_links'][$id]),
      '<SPAN CLASS="q-remove" STYLE="display:none">' . drupal_render($sub_form['remove_links'][$id]) . '</SPAN>',
    ),
    'attributes' => array(
      'class' => array(
        'links',
        'inline',
      ),
    ),
  ));
  $qnr_pid = $sub_form['qnr_pids'][$id]['#default_value'];
  $data_array = array(
    // The checkbox and the title.
    ($qnr_pid ? theme('indentation', array(
      'size' => 1,
    )) : NULL) . drupal_render($sub_form['titles'][$id]),
    $type,
    $action,
    isset($sub_form['revision'][$id]) ? drupal_render($sub_form['revision'][$id]) : t("Up to date"),
    drupal_render($sub_form['max_scores'][$id]),
    drupal_render($sub_form['auto_update_max_scores'][$id]),
    drupal_render($sub_form['stayers'][$id]),
  );
  if (isset($sub_form['compulsories'])) {
    $data_array[] = drupal_render($sub_form['compulsories'][$id]);
  }
  $data_array[] = drupal_render($sub_form['weights'][$id]);
  $data_array[] = drupal_render($sub_form['qnr_pids'][$id]);
  $data_array[] = array(
    'class' => array(
      'tabledrag-hide',
    ),
    'data' => drupal_render($sub_form['qnr_ids'][$id]),
  );
  $leaf_class = $sub_form['types'][$id]['#question_type'] != 'quiz_page' ? 'tabledrag-leaf' : '';
  return array(
    'class' => array(
      'q-row',
      'draggable',
      $leaf_class,
    ),
    'id' => 'q-' . $id,
    'data' => $data_array,
  );
}

/**
 * Adds inline js to automatically set the question's node title.
 */
function quiz_set_auto_title() {
  $max_length = variable_get('quiz_autotitle_length', 50);
  drupal_add_js(array(
    'quiz_max_length' => $max_length,
  ), array(
    'type' => 'setting',
  ));
  drupal_add_js(drupal_get_path('module', 'quiz') . '/js/quiz.auto-title.js');
}

/**
 * Admin page for feedback settings.
 */
function quiz_feedback_page() {
  $rows = array();
  $header = array(
    t('Time'),
    t('Description'),
    t('Conditions'),
  );
  foreach (quiz_get_feedback_times() as $key => $time) {
    $conditions = l(t('Conditions'), 'admin/quiz/feedback/manage/quiz_feedback_' . $key);
    $rows[] = array(
      $time['name'],
      $time['description'],
      $conditions,
    );
  }
  return theme('table', array(
    'caption' => t('Quiz feedback conditions'),
    'rows' => $rows,
    'header' => $header,
  ));
}

Functions

Namesort descending Description
quiz_admin_node_form Renders the quiz node form for the admin pages.
quiz_admin_node_form_submit Submit function for quiz_admin_node_form.
quiz_admin_node_form_validate Validation function for the quiz_admin_node_form form.
quiz_admin_results Quiz result report page for the quiz admin section.
quiz_admin_settings This builds the main settings form for the quiz module.
quiz_categorized_form Form for managing what questions should be added to a quiz with categorized random questions.
quiz_categorized_form_submit Submit the categorized form.
quiz_categorized_form_validate Validate the categorized form.
quiz_categorized_term_ahah Ahah function for finding terms...
quiz_feedback_page Admin page for feedback settings.
quiz_questions_form Handles "manage questions" tab.
quiz_questions_form_submit Submit function for quiz_questions.
quiz_questions_form_validate Validate that the supplied questions are real.
quiz_questions_page Creates a form for quiz questions.
quiz_settings_form_submit Submit the admin settings form.
quiz_settings_form_validate Validation of the Form Settings form.
quiz_set_auto_title Adds inline js to automatically set the question's node title.
_quiz_add_fields_for_creating_questions Fields for creating new questions are added to the quiz_questions_form.
_quiz_add_fields_for_random_quiz Add fields for random quiz to the quiz_questions_form.
_quiz_add_questions_to_form Adds the questions in the $questions array to the form.
_quiz_add_revision_checkbox Adds checkbox for creating new revision. Checks it by default if answers exists.
_quiz_categorized_add_term Adds a term to a categorized quiz.
_quiz_categorized_existing_terms_form
_quiz_categorized_new_term_form Form for adding new terms to a quiz.
_quiz_categorized_update_terms Update the categoriez belonging to a quiz with categorized random questions.
_quiz_get_id_from_string Searches for an id in the end of a string.
_quiz_get_question_row Helper function for theme_question_selection_table.
_quiz_question_browser_submit Takes care of the browser part of the submitted form values.
_quiz_search_terms Helper function for finding terms...
_quiz_update_items Update a quiz set of items with new weights and membership.