You are here

quiz.module in Quiz 8.6

Contains quiz.module

File

quiz.module
View source
<?php

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\quiz\Entity\QuizQuestionRelationship;
use Drupal\quiz\Entity\QuizResult;
use Drupal\quiz\Entity\QuizResultAnswerType;
use Drupal\quiz\Plugin\QuizQuestionPluginManager;

/**
 * @file
 * Contains quiz.module
 */

/**
 * Define question statuses...
 */
define('QUIZ_QUESTION_RANDOM', 0);
define('QUIZ_QUESTION_ALWAYS', 1);
define('QUIZ_QUESTION_NEVER', 2);

/**
 * Define options for keeping results.
 */
define('QUIZ_KEEP_BEST', 0);
define('QUIZ_KEEP_LATEST', 1);
define('QUIZ_KEEP_ALL', 2);

/**
 * Implements hook_help().
 */
function quiz_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.quiz':
      return t('<p>The quiz module allows users to administer a quiz, as a sequence of questions, and track the answers given. It allows for the creation of questions (and their answers), and organizes these questions into a quiz. Its target audience includes educational institutions, online training programs, employers, and people who just want to add a fun activity for their visitors to their Drupal site.</p>
<p>The quiz module has a plethora of permission options. Unless you take care setting your permissions, the quiz module might not do everything you want it to do.</p>
<p>For more information about quiz, and resources on how to use quiz, see the <a href="http://drupal.org/project/quiz">Quiz project website</a></p>');
  }
}

/**
 * Implements hook_cron().
 */
function quiz_cron() {
  $db = Drupal::database();
  $result_ids = array();

  // Remove old quiz results that haven't been finished.
  $rm_time = Drupal::config('quiz.settings')
    ->get('remove_partial_quiz_record');

  // $time = 0 for never.
  if ($rm_time) {
    $res = $db
      ->select('quiz_result', 'qnr')
      ->fields('qnr', array(
      'result_id',
    ))
      ->condition('time_end', 0)
      ->where('(:request_time - time_start) > :remove_time', array(
      ':request_time' => Drupal::time()
        ->getRequestTime(),
      ':remove_time' => $rm_time,
    ))
      ->execute();
    while ($result_id = $res
      ->fetchField()) {
      $result_ids[$result_id] = $result_id;
    }
  }

  // Remove invalid quiz results.
  $rm_time = Drupal::config('quiz.settings')
    ->get('remove_invalid_quiz_record');

  // $time = 0 for never.
  if ($rm_time) {
    $query = $db
      ->select('quiz_result', 'qnr');
    $query
      ->fields('qnr', array(
      'result_id',
    ));
    $query
      ->join('quiz', 'qnp', 'qnr.vid = qnp.vid');

    // If the user has a limited amount of takes we don't delete invalid
    // results.
    $db_or = $query
      ->orConditionGroup();
    $db_or
      ->isNull('qnp.takes');
    $db_or
      ->condition('qnp.takes', 0);
    $query
      ->condition($db_or);
    $query
      ->condition('qnr.is_invalid', 1);
    $query
      ->condition('qnr.time_end', Drupal::time()
      ->getRequestTime() - $rm_time, '<=');
    $res = $query
      ->execute();
    while ($result_id = $res
      ->fetchField()) {
      $result_ids[$result_id] = $result_id;
    }
  }
  $quiz_results = QuizResult::loadMultiple($result_ids);
  Drupal::entityTypeManager()
    ->getStorage('quiz_result')
    ->delete($quiz_results);
}

/**
 * Implements hook_menu().
 */
function quiz_menu() {
  $items['admin/quiz/reports'] = array(
    'title' => '@quiz reports and scoring',
    'title arguments' => array(
      '@quiz' => _quiz_get_quiz_name(),
    ),
    'description' => 'View reports and score answers.',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array(
      'view any quiz results',
      'view results for own quiz',
    ),
    'access callback' => 'quiz_access_multi_or',
    'type' => MENU_NORMAL_ITEM,
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  if (Drupal::moduleHandler()
    ->moduleExists('devel_generate')) {
    $items['admin/config/development/generate/quiz'] = array(
      'title' => 'Generate quiz',
      'description' => 'Generate a given number of quizzes and questions.',
      'access arguments' => array(
        'administer quiz configuration',
      ),
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'quiz_generate_form',
      ),
      'file' => 'quiz.devel.inc',
    );
  }
  return $items;
}

/**
 * Implements hook_theme().
 */
function quiz_theme($existing, $type, $theme, $path) {
  return array(
    'quiz_progress' => array(
      'variables' => array(
        'current' => NULL,
        'total' => NULL,
      ),
    ),
    'question_selection_table' => array(
      'render element' => 'form',
    ),
    'quiz_answer_result' => array(
      'variables' => array(),
    ),
    'quiz_report_form' => array(
      'render element' => 'form',
      'path' => $path . '/theme',
      'template' => 'quiz-report-form',
    ),
    'quiz_question_score' => array(
      'variables' => array(
        'score' => NULL,
        'max_score' => NULL,
        'class' => NULL,
      ),
      'template' => 'quiz-question-score',
    ),
    'quiz_jumper' => array(
      'variables' => array(
        'total' => 0,
        'current' => 0,
        'siblings' => 0,
      ),
    ),
    'quiz_pager' => array(
      'variables' => array(
        'total' => 0,
        'current' => 0,
        'siblings' => 0,
      ),
    ),
    'quiz_questions_page' => array(
      'render element' => 'form',
    ),
  );
}
function quiz_update_defaults($node) {
  $user = Drupal::currentUser();
  $entity = clone $node;
  if (Drupal::config('quiz.settings')
    ->get('use_passfail', 1)) {
    $entity->summary_pass = is_array($node->summary_pass) ? $node->summary_pass['value'] : $node->summary_pass;
    $entity->summary_pass_format = is_array($node->summary_pass) ? $node->summary_pass['format'] : $node->summary_pass_format;
  }
  $entity->summary_default = is_array($node->summary_default) ? $node->summary_default['value'] : $node->summary_default;
  $entity->summary_default_format = is_array($node->summary_default) ? $node->summary_default['format'] : $node->summary_default_format;

  // Save the node values.
  $quiz_props = clone $entity;
  $quiz_props->uid = 0;
  quiz_save_properties($quiz_props);
  if (!empty($node->remember_settings)) {

    // Save user defaults.
    $user_defaults = clone $quiz_props;
    $user_defaults->nid = 0;
    $user_defaults->vid = 0;
    $user_defaults->uid = $user
      ->id();
    quiz_save_properties($user_defaults);
  }
  if (!empty($node->remember_global)) {

    // Save global defaults.
    $global_defaults = clone $quiz_props;
    $global_defaults->uid = 0;
    $global_defaults->nid = 0;
    $global_defaults->vid = 0;
    quiz_save_properties($global_defaults);
  }
}

/**
 * Implements hook_field_extra_fields().
 */
function quiz_entity_extra_field_info() {
  $extra = array();
  $extra['quiz']['quiz'] = array(
    'display' => array(
      'take' => array(
        'label' => t('Take @quiz button', array(
          '@quiz' => _quiz_get_quiz_name(),
        )),
        'description' => t('The take button.'),
        'weight' => 10,
      ),
      'stats' => array(
        'label' => t('@quiz summary', array(
          '@quiz' => _quiz_get_quiz_name(),
        )),
        'description' => t('@quiz summary', array(
          '@quiz' => _quiz_get_quiz_name(),
        )),
        'weight' => 9,
      ),
    ),
  );

  // Allow for configurable feedback bits.
  $options = quiz_get_feedback_options();
  foreach (QuizResultAnswerType::loadMultiple() as $bundle) {
    $extra['quiz_result_answer'][$bundle
      ->id()]['display']['table'] = array(
      'label' => t('Feedback table'),
      'description' => t('A table of feedback.'),
      'weight' => 0,
      'visible' => TRUE,
    );
    foreach ($options as $option => $label) {
      $extra['quiz_result_answer'][$bundle
        ->id()]['display'][$option] = array(
        'label' => $label,
        'description' => t('Feedback for @label.', array(
          '@label' => $label,
        )),
        'weight' => 0,
        'visible' => FALSE,
      );
    }
  }
  $extra['quiz_result']['quiz_result']['display'] = array(
    'score' => array(
      'label' => t('Score'),
      'description' => t('The score of the result.'),
      'weight' => 1,
    ),
    'questions' => array(
      'label' => t('Questions'),
      'description' => t('The questions in this result.'),
      'weight' => 2,
    ),
    'summary' => array(
      'label' => t('Summary'),
      'description' => t('The summary and pass/fail text.'),
      'weight' => 3,
    ),
  );
  return $extra;
}

/**
 * Returns default values for all quiz settings.
 *
 * @todo also store this in the quiz_node_properties table
 *
 * @return
 *   Array of default values.
 */
function _quiz_get_node_defaults() {
  return (object) array(
    'allow_change' => 1,
    'allow_change_blank' => 0,
    'allow_jumping' => 0,
    'allow_resume' => 1,
    'allow_skipping' => 1,
    'always_available' => TRUE,
    'backwards_navigation' => 1,
    'build_on_last' => '',
    'keep_results' => 2,
    'mark_doubtful' => 0,
    'max_score' => 0,
    'max_score_for_random' => 1,
    'number_of_random_questions' => 0,
    'pass_rate' => 75,
    'quiz_always' => 1,
    'quiz_close' => 0,
    'quiz_open' => 0,
    'randomization' => 0,
    'repeat_until_correct' => 0,
    'review_options' => array(
      'question' => array(),
      'end' => array(
        'attempt' => 'attempt',
        'choice' => 'choice',
        'quiz_question_view_full' => 'quiz_question_view_full',
      ),
    ),
    'show_attempt_stats' => 1,
    'show_passed' => 1,
    'summary_default' => '',
    'summary_default_format' => filter_fallback_format(),
    'summary_pass' => '',
    'summary_pass_format' => filter_fallback_format(),
    'takes' => 0,
    'time_limit' => 0,
    'result_type' => 'quiz_result',
  );
}

/**
 * Implements hook_node_presave().
 */
function quiz_node_presave($node) {
  if ($node->type == 'quiz') {

    // Convert the action id to the actual id from the MD5 hash.
    // Why the actions module does this I do not know? Maybe to prevent
    // invalid values put into the options value="" field.
    if (!empty($node->aid) && ($aid = actions_function_lookup($node->aid))) {
      $node->aid = $aid;
    }
    if (Drupal::config('quiz.settings')
      ->get('auto_revisioning', 1)) {
      $node->revision = quiz_has_been_answered($node) ? 1 : 0;
    }

    // If this is a programmatic save, ensure we use the defaults.
    $defaults = quiz_get_defaults();
    foreach ($defaults as $property => $value) {
      if (!isset($node->{$property})) {
        $node->{$property} = $defaults->{$property};
      }
    }
  }
  if (isset($node->is_quiz_question) && Drupal::config('quiz.settings')
    ->get('auto_revisioning', 1)) {
    $node->revision = quiz_question_has_been_answered($node) ? 1 : 0;
  }
}

/**
 * Implements hook_node_prepare().
 */
function quiz_node_prepare($node) {
  if ($node->type == 'quiz' && !isset($node->nid)) {

    // If this is a new node we apply the user defaults for the quiz settings.
    if (arg(0) == 'node') {
      $user = Drupal::currentUser();
      if (!node_load_multiple(array(), array(
        'uid' => $user
          ->id(),
        'type' => 'quiz',
      ))) {
        Drupal::messenger()
          ->addMessage(t('You are making your first @quiz. On this page you set the attributes, most of which you may tell the system to remember as defaults for the future. On the next screen you can add questions.', array(
          '@quiz' => _quiz_get_quiz_name(),
        )));
      }
    }
    $settings = quiz_get_defaults();
    foreach ($settings as $key => $value) {
      if (!isset($node->{$key})) {
        $node->{$key} = $value;
      }
    }
  }
  if (isset($node->is_quiz_question)) {
    if (Drupal::config('quiz.settings')
      ->get('auto_revisioning', 1)) {
      $node->revision = quiz_question_has_been_answered($node) ? 1 : 0;
    }
  }
}

/**
 * Implements hook_user_cancel().
 *
 * Reassign Quiz results to the anonymous user, if requested.
 */
function quiz_user_cancel($edit, $account, $method) {
  if ($method == 'user_cancel_reassign') {
    db_query("UPDATE {quiz_result} SET uid = 0 WHERE uid = :uid", array(
      ':uid' => $account
        ->id(),
    ));
  }
}

/**
 * Implements hook_user_delete().
 */
function quiz_user_delete($account) {
  if (Drupal::config('quiz.settings')
    ->get('durod', 0)) {
    _quiz_delete_users_results($account
      ->id());
  }
}

/**
 * Deletes all results associated with a given user.
 *
 * @param int $uid
 *   The users id.
 */
function _quiz_delete_users_results($uid) {
  $res = db_query("SELECT result_id FROM {quiz_result} WHERE uid = :uid", array(
    ':uid' => $uid,
  ));
  $result_ids = array();
  while ($result_id = $res
    ->fetchField()) {
    $result_ids[] = $result_id;
  }
  entity_delete_multiple('quiz_result', $result_ids);
}

/**
 * @defgroup quiz_public Public API functions.
 * @{
 */

/**
 * Check a user/quiz combo to see if the user passed the given quiz.
 *
 * @param int $uid
 *   The user ID.
 * @param int $nid
 *   The node ID.
 * @param int $vid
 *   The version ID.
 *
 * @return bool
 *   Returns TRUE if the user has passed the quiz at least once, and FALSE
 *   otherwise. Note that a FALSE may simply indicate that the user has not
 *   taken the quiz.
 */
function quiz_is_passed($uid, $nid, $vid) {
  $passed = Drupal::database()
    ->query('SELECT COUNT(result_id) AS passed_count FROM {quiz_result} qnrs
    INNER JOIN {quiz} USING (vid, qid)
    WHERE qnrs.vid = :vid
      AND qnrs.qid = :qid
      AND qnrs.uid = :uid
      AND score >= pass_rate', array(
    ':vid' => $vid,
    ':qid' => $nid,
    ':uid' => $uid,
  ))
    ->fetchField();

  // Force into boolean context.
  return $passed !== FALSE && $passed > 0;
}

/**
 * Implements hook_quiz_access().
 *
 * Can a user take this quiz?
 */
function quiz_quiz_access(EntityInterface $entity, $operation, AccountInterface $account) {
  if ($operation == 'take') {
    $user_is_admin = $entity
      ->access('update');

    // Make sure this is available.
    if (!$entity
      ->get('quiz_date')
      ->isEmpty()) {

      // Compare current GMT time to the open and close dates (which should still
      // be in GMT time).
      $quiz_open = Drupal::time()
        ->getRequestTime() >= strtotime($entity
        ->get('quiz_date')
        ->get(0)
        ->getValue()['value']);
      $quiz_closed = Drupal::time()
        ->getRequestTime() >= strtotime($entity
        ->get('quiz_date')
        ->get(0)
        ->getValue()['end_value']);
      if (!$quiz_open || $quiz_closed) {
        if ($user_is_admin) {
          $hooks['admin_ignore_date'] = array(
            'success' => TRUE,
            'message' => (string) t('You are marked as an administrator or owner for this @quiz. While you can take this @quiz, the open/close times prohibit other users from taking this @quiz.', array(
              '@quiz' => _quiz_get_quiz_name(),
            )),
          );
        }
        else {
          if ($quiz_closed) {
            return AccessResultForbidden::forbidden((string) t('This @quiz is closed.', array(
              '@quiz' => _quiz_get_quiz_name(),
            )));
          }
          if (!$quiz_open) {
            return AccessResultForbidden::forbidden((string) t('This @quiz is not yet open.', array(
              '@quiz' => _quiz_get_quiz_name(),
            )));
          }
        }
      }
    }

    // Check to see if this user is allowed to take the quiz again:
    if ($entity
      ->get('takes')
      ->getString() > 0) {
      $taken = db_query("SELECT COUNT(*) AS takes FROM {quiz_result} WHERE uid = :uid AND qid = :qid", array(
        ':uid' => $account
          ->id(),
        ':qid' => $entity
          ->id(),
      ))
        ->fetchField();
      $t = Drupal::translation();
      $allowed_times = $t
        ->formatPlural($entity
        ->get('takes')
        ->getString(), '1 time', '@count times');
      $taken_times = $t
        ->formatPlural($taken, '1 time', '@count times');

      // The user has already taken this quiz.
      if ($taken) {
        if (false && $user_is_admin) {
          $hooks['owner_limit'] = array(
            'success' => TRUE,
            'message' => (string) t('You have taken this @quiz already. You are marked as an owner or administrator for this quiz, so you can take this quiz as many times as you would like.', array(
              '@quiz' => _quiz_get_quiz_name(),
            )),
          );
        }
        elseif ($taken >= $entity
          ->get('takes')
          ->getString()) {
          if ($entity->allow_resume && $entity
            ->getResumeableResult($account)) {

            // Quiz is resumable and there is an active attempt, so we should
            // allow them to finish it as it won't be creating a new attempt. This
            // is the blocker, so we do nothing here. The resume handles in the
            // take function.
          }
          elseif (!isset($_SESSION['quiz'][$entity
            ->id()])) {

            // If result is in session, don't check the attempt limit. @todo would
            // be to split up "take" into something like "start" and "continue" an
            // attempt.
            $hooks['attempt_limit'] = array(
              'success' => FALSE,
              'message' => (string) t('You have already taken this @quiz @really. You may not take it again.', array(
                '@quiz' => _quiz_get_quiz_name(),
                '@really' => $taken_times,
              )),
            );
          }
        }
        elseif ($entity->show_attempt_stats) {
          $hooks['attempt_limit'] = array(
            'success' => TRUE,
            'message' => (string) t("You can only take this @quiz @allowed. You have taken it @really.", array(
              '@quiz' => _quiz_get_quiz_name(),
              '@allowed' => $allowed_times,
              '@really' => $taken_times,
            )),
            'weight' => -10,
          );
        }
      }
    }

    // Check to see if the user is registered, and user alredy passed this quiz.
    if ($entity->show_passed && $account
      ->id() && quiz_is_passed($account
      ->id(), $entity
      ->id(), $entity
      ->getRevisionId())) {
      $hooks['already_passed'] = array(
        'success' => TRUE,
        'message' => (string) t('You have already passed this @quiz.', array(
          '@quiz' => _quiz_get_quiz_name(),
        )),
        'weight' => 10,
      );
    }
    if (!empty($hooks)) {
      foreach ($hooks as $hook) {
        if (!$hook['success']) {
          return AccessResultForbidden::forbidden($hook['message'], array(
            '@quiz' => _quiz_get_quiz_name(),
          ));
        }
      }
    }
    if (!empty($hooks)) {
      foreach ($hooks as $hook) {
        if ($hook['success']) {
          if (Drupal::routeMatch()
            ->getRouteName() == 'entity.quiz.canonical') {

            // Only display if we are viewing the quiz.
            Drupal::messenger()
              ->addWarning($hook['message']);
          }
          return [
            AccessResultAllowed::allowed($hook['message'], array(
              '@quiz' => _quiz_get_quiz_name(),
            )),
          ];
        }
      }
    }

    // Check permission and node access.
    if (!Drupal::currentUser()
      ->hasPermission('access quiz') || !$entity
      ->access('view')) {
      return [
        AccessResultForbidden::forbidden((string) t('You are not allowed to take this @quiz.', array(
          '@quiz' => _quiz_get_quiz_name(),
        ))),
      ];
    }
  }
}

/**
 * @} End of "defgroup quiz_public".
 */

/**
 * Retrieve list of question types.
 *
 * @return
 *   Array of question types.
 */
function quiz_get_question_types() {
  $pluginManager = Drupal::service('plugin.manager.quiz.question');
  $plugins = $pluginManager
    ->getDefinitions();
  if (empty($plugins)) {
    Drupal::messenger()
      ->addWarning(t('You need to install and enable at least one question type to use Quiz.'));
  }
  return $plugins;
}

/**
 * Get sub-questions.
 */
function quiz_get_sub_questions($qqr_pid, &$questions) {
  $query = db_select('node', 'n');
  $query
    ->fields('n', array(
    'nid',
    'type',
  ));
  $query
    ->fields('nr', array(
    'vid',
    'title',
  ));
  $query
    ->fields('qnr', array(
    'question_status',
    'weight',
    'max_score',
    'auto_update_max_score',
    'qnr_id',
    'qqr_pid',
    'child_nid',
    'child_vid',
  ));
  $query
    ->addField('n', 'vid', 'latest_vid');
  $query
    ->innerJoin('node_revision', 'nr', 'n.nid = nr.nid');
  $query
    ->innerJoin('quiz_question_relationship', 'qnr', 'nr.vid = qnr.child_vid');
  $query
    ->condition('qqr_pid', $qqr_pid);
  $query
    ->orderBy('weight');
  $result = $query
    ->execute();
  foreach ($result as $question) {
    $questions[] = $question;
  }
}

/**
 * Get a list of all available quizzes.
 *
 * @param $uid
 *   An optional user ID. If supplied, only quizzes created by that user will be
 *   returned.
 *
 * @return
 *   A list of quizzes.
 */
function _quiz_get_quizzes($uid = 0) {
  $results = array();
  $args = array();
  $query = db_select('node', 'n')
    ->fields('n', array(
    'nid',
    'vid',
    'title',
    'uid',
    'created',
  ))
    ->fields('u', array(
    'name',
  ));
  $query
    ->leftJoin('users', 'u', 'u.uid = n.uid');
  $query
    ->condition('n.type', 'quiz');
  if ($uid != 0) {
    $query
      ->condition('n.uid', $uid);
  }
  $query
    ->orderBy('n.nid');
  $quizzes = $query
    ->execute();
  foreach ($quizzes as $quiz) {
    $results[$quiz->nid] = (array) $quiz;
  }
  return $results;
}

/**
 * Get the quiz name variable and set it as a constant so we don't have to keep
 * calling it in every function.
 *
 * @return string
 *   Quiz name variable.
 */
function _quiz_get_quiz_name() {
  $quiz = Drupal::entityTypeManager()
    ->getDefinition('quiz');
  return $quiz
    ->getLabel();
}

/**
 * Copies questions when a quiz is translated.
 *
 * @param $node
 *   The new translated quiz node.
 */
function quiz_copy_questions($node) {

  // Find original questions.
  $query = db_query('SELECT child_nid, child_vid, question_status, weight, max_score, auto_update_max_score
    FROM {quiz_node_relationship}
    WHERE parent_vid = :parent_vid', array(
    ':parent_vid' => $node->translation_source->vid,
  ));
  foreach ($query as $res_o) {
    $original_question = node_load($res_o->child_nid);

    // Set variables we can't or won't carry with us to the translated node to
    // NULL.
    $original_question->nid = $original_question->vid = $original_question->created = $original_question->changed = NULL;
    $original_question->revision_timestamp = $original_question->menu = $original_question->path = NULL;
    $original_question->files = array();
    if (isset($original_question->book['mlid'])) {
      $original_question->book['mlid'] = NULL;
    }

    // Set the correct language.
    $original_question->language = $node->language;

    // Save the node.
    node_save($original_question);

    // Save the relationship between the new question and the quiz.
    $quiz_question_relationship = (object) array(
      'parent_nid' => $node->nid,
      'parent_vid' => $node->vid,
      'child_nid' => $original_question->nid,
      'child_vid' => $original_question->vid,
      'question_status' => $res_o->question_status,
      'weight' => $res_o->weight,
      'max_score' => $res_o->max_score,
      'auto_update_max_score' => $res_o->auto_update_max_score,
    );
    entity_save('quiz_question_relationship', $quiz_question_relationship);
  }
}

/**
 * Returns the users default settings.
 *
 * @param $node
 *   Quiz node.
 * @param $uid
 *   (optional) The uid of the user to get the settings for. Defaults to the
 *   current user (NULL).
 *
 * @return
 *   An array of settings. The array is empty in case no settings are available.
 */
function quiz_get_defaults() {
  $user = Drupal::currentUser();
  $entity = entity_load('quiz', FALSE, array(
    'uid' => $user
      ->id(),
    'nid' => 0,
    'vid' => 0,
  ), TRUE);
  if (count($entity)) {

    // We found user defaults.
    $defaults = clone reset($entity);
    unset($defaults->nid, $defaults->uid, $defaults->vid);
    return $defaults;
  }
  $entity = entity_load('quiz', FALSE, array(
    'uid' => 0,
    'nid' => 0,
    'vid' => 0,
  ), TRUE);
  if (count($entity)) {

    // Found global defaults.
    $defaults = clone reset($entity);
    unset($defaults->nid, $defaults->uid, $defaults->vid);
    return $defaults;
  }

  // No defaults set yet.
  return _quiz_get_node_defaults();
}

/**
 * Format a number of seconds to a hh:mm:ss format.
 *
 * @param $time_in_sec
 *   Integers time in seconds.
 *
 * @return string
 *   String time in min : sec format.
 */
function _quiz_format_duration($time_in_sec) {
  $hours = intval($time_in_sec / 3600);
  $min = intval(($time_in_sec - $hours * 3600) / 60);
  $sec = $time_in_sec % 60;
  if (strlen($min) == 1) {
    $min = '0' . $min;
  }
  if (strlen($sec) == 1) {
    $sec = '0' . $sec;
  }
  return "{$hours}:{$min}:{$sec}";
}

/**
 * Get the feedback options for Quizzes.
 */
function quiz_get_feedback_options() {
  $feedback_options = Drupal::moduleHandler()
    ->invokeAll('quiz_feedback_options');
  $view_modes = Drupal::service('entity_display.repository')
    ->getViewModes('quiz_question');
  $feedback_options["quiz_question_view_full"] = t('Question') . ': ' . 'Full';
  foreach ($view_modes as $view_mode => $info) {
    $feedback_options["quiz_question_view_" . $view_mode] = t('Question') . ': ' . $info['label'];
  }
  $feedback_options += array(
    'attempt' => t('Attempt'),
    'choice' => t('Choices'),
    'correct' => t('Whether correct'),
    'score' => t('Score'),
    'answer_feedback' => t('Answer feedback'),
    'question_feedback' => t('Question feedback'),
    'solution' => t('Correct answer'),
    'quiz_feedback' => t('@quiz feedback', array(
      '@quiz' => _quiz_get_quiz_name(),
    )),
  );
  Drupal::moduleHandler()
    ->alter('quiz_feedback_options', $feedback_options);
  return $feedback_options;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds a checkbox for controlling field edit access to fields added to
 * quizzes.
 */
function quiz_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
  $field = $form_state
    ->getFormObject()
    ->getEntity();
  if ($field
    ->getTargetEntityTypeId() != 'quiz_result') {
    return;
  }
  $form['third_party_settings']['quiz']['show_field'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show this field on @quiz start.', array(
      '@quiz' => _quiz_get_quiz_name(),
    )),
    '#default_value' => $field
      ->getThirdPartySetting('quiz', 'show_field', TRUE),
    '#description' => t('If checked, this field will be presented when starting a quiz.'),
  );
}

/**
 * Implements hook_field_access().
 *
 * Don't show the user fields that weren't marked as quiz result fields.
 */
function quiz_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  if ($field_definition
    ->getTargetEntityTypeId() == 'quiz_result') {
    if (is_a($field_definition, FieldConfig::class)) {

      /* @var $field_definition FieldConfig */
      if (!$field_definition
        ->getThirdPartySetting('quiz', 'show_field')) {
        return AccessResult::forbidden('quiz_show_field');
      }
    }
  }
  return AccessResult::neutral();
}

/**
 * Implements hook_entity_bundle_info().
 *
 * @todo doesn't work yet
 *
 * Define a question and answer bundle for each question type so they don't have
 * to do it in configuration.
 */
function quiz_entity_bundle_info() {

  /* @var $type QuizQuestionPluginManager */
  $type = Drupal::service('plugin.manager.quiz.question');
  $question_types = $type
    ->getDefinitions();
  $bundles = array();
  foreach ($question_types as $key => $question_type) {
    $bundles['quiz_question'][$key] = [
      'label' => $question_type['label'],
    ];
  }

  //return $bundles;
}

/**
 * Implements hook_page_attachments().
 */
function quiz_page_attachments(&$page) {
  $page['#attached']['library'][] = 'quiz/styles';
}

/**
 * Implements hook_query_TAG_alter().
 *
 * Add randomization to the categorized question build.
 */
function quiz_query_random_alter(AlterableInterface $query) {
  $query
    ->orderRandom();
}

/**
 * Help us with special pagination.
 *
 * Why not the Drupal theme_pager()?
 *
 * It uses query strings. We have access on each menu argument (quiz question
 * number) so we unfortunately cannot use it.
 */
function _quiz_pagination_helper($total, $perpage = NULL, $current = NULL, $siblings = NULL) {
  $result = array();
  if (isset($total, $perpage) === TRUE) {
    $result = range(1, ceil($total / $perpage));
    if (isset($current, $siblings) === TRUE) {
      if (($siblings = floor($siblings / 2) * 2 + 1) >= 1) {
        $result = array_slice($result, max(0, min(count($result) - $siblings, intval($current) - ceil($siblings / 2))), $siblings);
      }
    }
  }
  return $result;
}

Functions

Namesort descending Description
quiz_copy_questions Copies questions when a quiz is translated.
quiz_cron Implements hook_cron().
quiz_entity_bundle_info Implements hook_entity_bundle_info().
quiz_entity_extra_field_info Implements hook_field_extra_fields().
quiz_entity_field_access Implements hook_field_access().
quiz_form_field_config_edit_form_alter Implements hook_form_FORM_ID_alter().
quiz_get_defaults Returns the users default settings.
quiz_get_feedback_options Get the feedback options for Quizzes.
quiz_get_question_types Retrieve list of question types.
quiz_get_sub_questions Get sub-questions.
quiz_help Implements hook_help().
quiz_is_passed Check a user/quiz combo to see if the user passed the given quiz.
quiz_menu Implements hook_menu().
quiz_node_prepare Implements hook_node_prepare().
quiz_node_presave Implements hook_node_presave().
quiz_page_attachments Implements hook_page_attachments().
quiz_query_random_alter Implements hook_query_TAG_alter().
quiz_quiz_access Implements hook_quiz_access().
quiz_theme Implements hook_theme().
quiz_update_defaults
quiz_user_cancel Implements hook_user_cancel().
quiz_user_delete Implements hook_user_delete().
_quiz_delete_users_results Deletes all results associated with a given user.
_quiz_format_duration Format a number of seconds to a hh:mm:ss format.
_quiz_get_node_defaults Returns default values for all quiz settings.
_quiz_get_quizzes Get a list of all available quizzes.
_quiz_get_quiz_name Get the quiz name variable and set it as a constant so we don't have to keep calling it in every function.
_quiz_pagination_helper Help us with special pagination.

Constants

Namesort descending Description
QUIZ_KEEP_ALL
QUIZ_KEEP_BEST Define options for keeping results.
QUIZ_KEEP_LATEST
QUIZ_QUESTION_ALWAYS
QUIZ_QUESTION_NEVER
QUIZ_QUESTION_RANDOM Define question statuses...