View source
<?php
namespace Drupal\quiz\Entity;
use Drupal;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\quiz\Entity\QuizQuestion;
use Drupal\quiz\Entity\QuizQuestionRelationship;
use Drupal\quiz\Entity\QuizResult;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\EntityOwnerTrait;
use const QUIZ_QUESTION_ALWAYS;
use function _quiz_get_random_questions;
use function count;
class Quiz extends EditorialContentEntityBase implements EntityChangedInterface, EntityOwnerInterface, RevisionLogInterface, EntityPublishedInterface {
use EntityOwnerTrait;
use EntityChangedTrait;
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::ownerBaseFieldDefinitions($entity_type);
$fields['title'] = BaseFieldDefinition::create('string')
->setLabel(t('Title'))
->setDescription(t('This is only visible to Quiz admnistrators.'))
->setRequired(TRUE)
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
]);
$fields['body'] = BaseFieldDefinition::create('text_long')
->setLabel(t('Description'))
->setSetting('weight', 0)
->setRequired(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE)
->setDisplayOptions('form', [
'type' => 'text_textarea',
]);
$fields['created'] = BaseFieldDefinition::create('created')
->setRevisionable(TRUE)
->setLabel('Created');
$fields['changed'] = BaseFieldDefinition::create('changed')
->setRevisionable(TRUE)
->setLabel('Changed');
$fields['number_of_random_questions'] = BaseFieldDefinition::create('integer')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setRevisionable(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'number',
])
->setLabel(t('Number of random questions'));
$fields['max_score_for_random'] = BaseFieldDefinition::create('integer')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'number',
])
->setLabel(t('Max score for random'));
$fields['pass_rate'] = BaseFieldDefinition::create('integer')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'number',
])
->setSettings([
'min' => 0,
'max' => 100,
])
->setDescription('Passing rate for this Quiz as a percentage score.')
->setLabel(t('Passing rate for Quiz (%)'));
$fields['summary_pass'] = BaseFieldDefinition::create('text_long')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'text_textarea',
])
->setDescription('Summary text for when the user passes the Quiz. Leave blank to not give summary text if passed, or if not using the "passing rate" field above. If not using the "passing rate" field above, this text will not be used.')
->setLabel(t('Result text for a passing grade.'));
$fields['summary_default'] = BaseFieldDefinition::create('text_long')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'text_textarea',
])
->setDescription('Summary text for when the user fails the Quiz. Leave blank to not give summary text if failed, or if not using the "passing rate" field above. If not using the "passing rate" field above, this text will not be used.')
->setLabel(t('Result text for any grade.'));
$fields['randomization'] = BaseFieldDefinition::create('list_integer')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'options_buttons',
])
->setCardinality(1)
->setSetting('allowed_values', [
0 => 'No randomization',
1 => 'Random order',
2 => 'Random questions',
3 => 'Categorized random questions',
])
->setDescription("<strong>Random order</strong> - all questions display in random order<br>\n<strong>Random questions</strong> - specific number of questions are drawn randomly from this Quiz's pool of questions<br>\n<strong>Categorized random questions</strong> - specific number of questions are drawn from each specified taxonomy term")
->setDefaultValue(0)
->setRequired(TRUE)
->setLabel(t('Randomize questions'));
$fields['backwards_navigation'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Allow users to go back and revisit questions already answered.')
->setLabel(t('Backwards navigation'));
$fields['keep_results'] = BaseFieldDefinition::create('list_integer')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setCardinality(1)
->setDefaultValue(2)
->setRequired(TRUE)
->setDisplayOptions('form', [
'type' => 'options_buttons',
])
->setSetting('allowed_values', [
0 => 'The best',
1 => 'The newest',
2 => 'All',
])
->setLabel(t('Store results'))
->setDescription('These results should be stored for each user.');
$fields['repeat_until_correct'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Require the user to retry the question until answered correctly.')
->setLabel(t('Repeat until correct'));
$fields['quiz_date'] = BaseFieldDefinition::create('daterange')
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'daterange_default',
])
->setDescription('The date and time during which this Quiz will be available. Leave blank to always be available.')
->setLabel(t('Quiz date'));
$fields['takes'] = BaseFieldDefinition::create('integer')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'number',
])
->setLabel(t('Allowed number of attempts'))
->setDescription('The number of times a user is allowed to take this Quiz. Anonymous users are only allowed to take Quiz that allow an unlimited number of attempts.');
$fields['show_attempt_stats'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setSetting('min', 0)
->setDescription('Display the allowed number of attempts on the starting page for this Quiz.')
->setLabel(t('Display allowed number of attempts'));
$fields['time_limit'] = BaseFieldDefinition::create('integer')
->setDisplayConfigurable('view', TRUE)
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'number',
])
->setSetting('min', 0)
->setDescription('Set the maximum allowed time in seconds for this Quiz. Use 0 for no limit.')
->setLabel(t('Time limit'));
$fields['max_score'] = BaseFieldDefinition::create('integer')
->setRevisionable(TRUE)
->setLabel(t('Calculated max score of this quiz.'));
$fields['allow_skipping'] = BaseFieldDefinition::create('boolean')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Allow users to skip questions in this Quiz.')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setLabel(t('Allow skipping'));
$fields['allow_resume'] = BaseFieldDefinition::create('boolean')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Allow users to leave this Quiz incomplete and then resume it from where they left off.')
->setLabel(t('Allow resume'));
$fields['allow_jumping'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Allow users to jump to any question using a menu or pager in this Quiz.')
->setLabel(t('Allow jumping'));
$fields['allow_change'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('If the user is able to visit a previous question, allow them to change the answer.')
->setLabel(t('Allow changing answers'));
$fields['allow_change_blank'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Allow users to go back and revisit questions already answered.')
->setLabel(t('Allow changing blank answers'));
$fields['build_on_last'] = BaseFieldDefinition::create('list_string')
->setLabel('Each attempt builds on the last')
->setDisplayConfigurable('form', TRUE)
->setDefaultValue('fresh')
->setRequired(TRUE)
->setCardinality(1)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'options_buttons',
])
->setSetting('allowed_values', [
'fresh' => t('Fresh attempt every time'),
'correct' => t('Prepopulate with correct answers from last result'),
'all' => t('Prepopulate with all answers from last result'),
])
->setDescription('Instead of starting a fresh Quiz, users can base a new attempt on the last attempt, with correct answers prefilled. Set the default selection users will see. Selecting "fresh attempt every time" will not allow the user to choose.');
$fields['show_passed'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Show a message if the user has previously passed the Quiz.')
->setLabel(t('Show passed message'));
$fields['mark_doubtful'] = BaseFieldDefinition::create('boolean')
->setDefaultValueCallback('\\Drupal\\quiz\\Util\\QuizUtil::baseFieldDefault')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDescription('Allow users to mark their answers as doubtful.')
->setLabel(t('Mark doubtful'));
$fields['review_options'] = BaseFieldDefinition::create('map')
->setRevisionable(TRUE)
->setLabel(t('Review options'));
$fields['result_type'] = BaseFieldDefinition::create('entity_reference')
->setSetting('target_type', 'quiz_result_type')
->setRequired(TRUE)
->setDefaultValue('quiz_result')
->setDisplayConfigurable('form', TRUE)
->setRevisionable(TRUE)
->setLabel(t('Result type to use'));
$fields['result_options'] = BaseFieldDefinition::create('entity_reference_revisions')
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
->setSetting('target_type', 'paragraph')
->setSetting('handler_settings', [
'target_bundles' => [
'quiz_result_feedback' => 'quiz_result_feedback',
],
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'entity_reference_paragraphs',
])
->setLabel('Result options');
$fields['quiz_terms'] = BaseFieldDefinition::create('entity_reference_revisions')
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
->setSetting('target_type', 'paragraph')
->setSetting('handler_settings', [
'target_bundles' => [
'quiz_question_term_pool' => 'quiz_question_term_pool',
],
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'entity_reference_paragraphs',
])
->setLabel('Quiz terms');
return $fields;
}
function addQuestion(QuizQuestion $quiz_question) {
$relationships = Drupal::entityTypeManager()
->getStorage('quiz_question_relationship')
->loadByProperties([
'quiz_id' => $this
->id(),
'quiz_vid' => $this
->getRevisionId(),
'question_id' => $quiz_question
->id(),
'question_vid' => $quiz_question
->getRevisionId(),
]);
if (empty($relationships)) {
$qqr = QuizQuestionRelationship::create([
'quiz_id' => $this
->id(),
'quiz_vid' => $this
->getRevisionId(),
'question_id' => $quiz_question
->id(),
'question_vid' => $quiz_question
->getRevisionId(),
]);
$qqr
->save();
return $qqr;
}
else {
return reset($relationships);
}
}
function getResumeableResult(AccountInterface $user) {
$query = Drupal::entityQuery('quiz_result')
->condition('qid', $this
->get('qid')
->getString())
->condition('uid', $user
->id())
->condition('time_end', NULL, 'IS NULL')
->sort('time_start', 'DESC')
->range(0, 1);
if ($result = $query
->execute()) {
return QuizResult::load(key($result));
}
return NULL;
}
public function delete() {
$entities = \Drupal::entityTypeManager()
->getStorage('quiz_question_relationship')
->loadByProperties([
'quiz_id' => $this
->id(),
]);
foreach ($entities as $entity) {
$entity
->delete();
}
$entities = \Drupal::entityTypeManager()
->getStorage('quiz_result')
->loadByProperties([
'qid' => $this
->id(),
]);
foreach ($entities as $entity) {
$entity
->delete();
}
parent::delete();
}
function buildLayout() {
$questions = array();
if ($this
->get('randomization')
->getString() == 3) {
$questions = $this
->_quiz_build_categorized_question_list();
}
else {
$query = \Drupal::database()
->query('SELECT qqr.question_id as qqid, qqr.question_vid as vid, qq.type, qqr.qqr_id, qqr.qqr_pid, qq.title
FROM {quiz_question_relationship} qqr
JOIN {quiz_question} qq ON qqr.question_id = qq.qqid
LEFT JOIN {quiz_question_relationship} qqr2 ON (qqr.qqr_pid = qqr2.qqr_id OR (qqr.qqr_pid IS NULL AND qqr.qqr_id = qqr2.qqr_id))
WHERE qqr.quiz_vid = :quiz_vid
AND qqr.question_status = :question_status
ORDER BY qqr2.weight, qqr.weight', array(
':quiz_vid' => $this
->getRevisionId(),
':question_status' => QUIZ_QUESTION_ALWAYS,
));
$i = 0;
while ($question_node = $query
->fetchAssoc()) {
$i++;
$questions[$i] = $question_node;
}
if ($this
->get('randomization')
->getString() == 2) {
if ($this
->get('number_of_random_questions')
->getString() > 0) {
$random_questions = $this
->getRandomQuestions();
$questions = array_merge($questions, $random_questions);
if ($this
->get('number_of_random_questions')
->getString() > count($random_questions)) {
return FALSE;
}
}
}
if ($this
->get('randomization')
->getString() == '1') {
$question_to_shuffle = array();
$mark = NULL;
foreach ($questions as $qidx => $question) {
if ($mark) {
if ($question['type'] == 'page') {
shuffle($question_to_shuffle);
array_splice($questions, $mark, $qidx - $mark - 1, $question_to_shuffle);
$mark = 0;
$question_to_shuffle = array();
}
else {
$question_to_shuffle[] = $question;
}
}
if ($question['type'] == 'page') {
$mark = $qidx;
}
}
if ($mark) {
shuffle($question_to_shuffle);
array_splice($questions, $mark, $qidx - $mark, $question_to_shuffle);
}
elseif (is_null($mark)) {
shuffle($questions);
}
}
}
$count = 0;
$display_count = 0;
$questions_out = array();
foreach ($questions as &$question) {
$count++;
$display_count++;
$question['number'] = $count;
if ($question['type'] != 'page') {
$question['display_number'] = $display_count;
}
$questions_out[$count] = $question;
}
return $questions_out;
}
function hasAttempts() {
$result = \Drupal::entityQuery('quiz_result')
->condition('qid', $this
->id())
->condition('vid', $this
->getRevisionId())
->range(0, 1)
->execute();
return !empty($result);
}
function _quiz_build_categorized_question_list() {
$terms = $this
->get('quiz_terms')
->referencedEntities();
$total_questions = [];
foreach ($terms as $term) {
$tid = $term
->get('quiz_question_tid')
->referencedEntities()[0]
->id();
$query = \Drupal::entityQuery('quiz_question');
$fields = \Drupal::service('entity_field.manager')
->getFieldStorageDefinitions('quiz_question');
$or = $query
->orConditionGroup();
foreach ($fields as $field_name => $field) {
if ($field
->getType() == 'entity_reference' && $field
->getSetting('target_type') == 'taxonomy_term') {
$or
->condition("{$field_name}.target_id", $tid);
}
}
$query
->condition($or);
$query
->condition('status', 1);
$query
->addTag('quiz_build_categorized_questions');
$query
->addTag('random');
$query
->range(0, $term
->get('quiz_question_number')
->getString());
$question_ids = $query
->execute();
if (count($question_ids) != $term
->get('quiz_question_number')
->getString()) {
return [];
}
$found_questions = QuizQuestion::loadMultiple($question_ids);
foreach ($found_questions as $qqid => $question) {
$total_questions[] = [
'qqid' => $qqid,
'tid' => $tid,
'type' => $question
->bundle(),
'vid' => $question
->getRevisionId(),
];
}
}
return $total_questions;
}
function getNumberOfRequiredQuestions() {
$query = Drupal::entityQuery('quiz_question_relationship');
$query
->condition('quiz_vid', $this
->getRevisionId());
$query
->condition('question_status', QUIZ_QUESTION_ALWAYS);
$result = $query
->execute();
return count($result);
}
function getNumberOfQuestions() {
$count = 0;
$relationships = $this
->getQuestions();
$random = $this
->get('randomization')
->getString();
switch ($random) {
case 2:
case 3:
$count = $this
->getNumberOfRequiredQuestions() + $this
->get('number_of_random_questions')->value;
break;
case 0:
case 1:
default:
foreach ($relationships as $relationship) {
if ($quizQuestion = $relationship
->getQuestion()) {
if ($quizQuestion
->isGraded()) {
$count++;
}
}
}
}
return intval($count);
}
function isLastQuestion() {
$quiz_result = QuizResult::load($_SESSION['quiz'][$this
->id()]['result_id']);
$current = $_SESSION['quiz'][$this
->id()]['current'];
$layout = $quiz_result
->getLayout();
foreach ($layout as $idx => $qra) {
if ($qra
->get('question_id')
->referencedEntities()[0]
->bundle() == 'page') {
if ($current == $idx) {
$in_page = TRUE;
$last_page = TRUE;
}
else {
$last_page = FALSE;
}
}
elseif (empty($qra->qqr_pid)) {
$in_page = FALSE;
$last_page = FALSE;
}
}
return $last_page || !isset($layout[$_SESSION['quiz'][$this
->id()]['current'] + 1]);
}
function createDuplicate() {
$vid = $this
->getRevisionId();
$dupe = parent::createDuplicate();
$dupe->old_vid = $vid;
return $dupe;
}
function getQuestions() {
$relationships = Drupal::entityTypeManager()
->getStorage('quiz_question_relationship')
->loadByProperties([
'quiz_id' => $this
->id(),
'quiz_vid' => $this
->getRevisionId(),
]);
return $relationships;
$questions = array();
$query = db_select('quiz_question', 'n');
$query
->fields('n', array(
'qqid',
'type',
));
$query
->fields('nr', array(
'vid',
'title',
));
$query
->fields('qnr', array(
'question_status',
'weight',
'max_score',
'auto_update_max_score',
'qnr_id',
'qqr_pid',
'question_id',
'question_vid',
));
$query
->addField('n', 'vid', 'latest_vid');
$query
->join('quiz_question_revision', 'nr', 'n.qid = nr.qid');
$query
->leftJoin('quiz_question_relationship', 'qnr', 'nr.vid = qnr.child_vid');
$query
->condition('n.status', 1);
$query
->condition('qnr.quiz_id', $quiz_nid);
if ($quiz_vid) {
$query
->condition('qnr.quiz_vid', $quiz_vid);
}
$query
->condition('qqr_pid', NULL, 'IS');
$query
->orderBy('qnr.weight');
$result = $query
->execute();
foreach ($result as $question) {
$questions[] = $question;
quiz_get_sub_questions($question->qnr_id, $questions);
}
return $questions;
}
function copyFromRevision(Quiz $old_quiz) {
$quiz_questions = \Drupal::entityTypeManager()
->getStorage('quiz_question_relationship')
->loadByProperties([
'quiz_vid' => $old_quiz
->getRevisionId(),
]);
foreach ($quiz_questions as $quiz_question) {
$new_question = $quiz_question
->createDuplicate();
$new_question
->set('quiz_vid', $this
->getRevisionId());
$new_question
->set('quiz_id', $this
->id());
$old_id = $quiz_question
->id();
$new_question
->save();
$new_questions[$old_id] = $new_question;
}
foreach ($new_questions as $old_id => $quiz_question) {
if (!$quiz_question
->get('qqr_pid')
->isEmpty()) {
$quiz_question
->set('qqr_pid', $new_questions[$quiz_question
->get('qqr_pid')
->getString()]
->id());
$quiz_question
->save();
}
}
}
function getRandomQuestions() {
$num_random = $this
->get('number_of_random_questions')
->getString();
$questions = array();
if ($num_random > 0) {
$query = Drupal::entityQuery('quiz_question_relationship');
$query
->condition('quiz_vid', $this
->getRevisionId());
$query
->condition('question_status', QUIZ_QUESTION_RANDOM);
$query
->addTag('random');
$query
->range(0, $this
->get('number_of_random_questions')
->getString());
if ($relationships = $query
->execute()) {
$qqrs = QuizQuestionRelationship::loadMultiple($relationships);
foreach ($qqrs as $qqr) {
$questionEntity = Drupal::entityTypeManager()
->getStorage('quiz_question')
->loadRevision($qqr
->get('question_vid')
->getString());
$question = [
'qqid' => $questionEntity
->id(),
'vid' => $questionEntity
->getRevisionId(),
'type' => $questionEntity
->bundle(),
'random' => TRUE,
'relative_max_score' => $this
->get('max_score_for_random')
->getString(),
];
$questions[] = $question;
}
return $questions;
}
}
return [];
}
}