quiz-6.0.0-alpha4/src/Form/QuizQuestionAnsweringForm.php

src/Form/QuizQuestionAnsweringForm.php
<?php

namespace Drupal\quiz\Form;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\quiz\Services\QuizSessionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * The form used to deliver questions to users and capture their responses.
 */
class QuizQuestionAnsweringForm extends FormBase {

  use MessengerTrait;

  /**
   * QuizQuestionAnsweringForm constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\quiz\Services\QuizSessionInterface $quizSession
   *   The quiz session service.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected QuizSessionInterface $quizSession,
    protected TimeInterface $time,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('quiz.session'),
      $container->get('datetime.time'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    /** @var \Drupal\quiz\Entity\QuizQuestion[] $questions */
    $questions = $form_state->getBuildInfo()['args'][0];
    $quiz_session = $this->quizSession;

    /** @var \Drupal\quiz\Entity\QuizResult $quiz_result */
    $quiz_result = $form_state->getBuildInfo()['args'][1];
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
    $storage = $this->entityTypeManager->getStorage('quiz');
    $quiz = $storage->loadRevision($quiz_result->get('vid')->getString());

    // Store quiz and result in the form.
    $form['#quiz'] = $quiz;
    $form['#quiz_result'] = $quiz_result;

    if (!is_array($questions)) {
      /** @var \Drupal\quiz\Entity\QuizQuestion $questions */
      // One single question (or page?)
      if ($questions->bundle() == 'page') {
        foreach ($quiz_result->getLayout() as $qra) {
          if ($qra->get('question_id')->getString() == $questions->id()) {
            // Found the page in the layout.
            // Add the page as the first "question".
            $questions = [$questions];
            foreach ($quiz_result->getLayout() as $qra2) {
              // Get all page children.
              if ($qra2->qqr_pid == $qra->qqr_id) {
                // This question belongs in the requested page.
                $questions[] = $qra2->getQuizQuestion();
              }
            }
            break;
          }
        }
        unset($qra, $qra2);
      }
      else {
        // Make this a page of 1 question.
        $questions = [$questions->id() => $questions];
      }
    }

    $form['#attributes']['class'] = ['answering-form'];
    $form['#tree'] = TRUE;

    // Mark this as the current question.
    $quiz_result->setQuestion(reset($questions)
      ->getResponse($quiz_result)
      ->get('number')->value);

    foreach ($questions as $question) {
      $class = Html::getClass('quiz-question-' . $question->bundle());
      // Element for a single question.
      // @todo Instead of doing this load I think we can refer back to the layout.
      $qra = $question->getResponse($quiz_result);

      $element = $question->getAnsweringForm($form_state, $qra);

      // Render the question using the "question" view mode.
      $build = $this->entityTypeManager
        ->getViewBuilder('quiz_question')
        ->view($question, 'question');

      $header_markup = NULL;
      if (!$qra->get('display_number')->isEmpty()) {
        $header_markup = ['#markup' => "<h2>" . $this->t("Question @question", ['@question' => $qra->get('display_number')->getString()]) . "</h2>"];
      }

      $form['question'][$question->id()] = [
        '#attributes' => ['class' => [$class]],
        '#type' => 'container',
        'header' => $header_markup,
        'question' => $build,
        'answer' => $element,
      ];
      $form['question'][$question->id()]['answer']['#quiz_result_answer'] = $qra;

      $blank_and_change = $qra->get('is_skipped')->getString() && $quiz->get('allow_change_blank')->getString();
      if (!$quiz->get('allow_change')->getString() && !$qra->get('answer_timestamp')->isEmpty()) {
        if ($blank_and_change) {
          // Allow it.
        }
        else {
          // This question was already answered, or answering blank question is
          // disabled.
          $form['question'][$question->id()]['#disabled'] = TRUE;
          if (empty($form_state->getUserInput())) {
            // Only show message if not submitting.
            $this->messenger()->addWarning($this->t('Changing answers is disabled.'));
          }
        }
      }

      if ($quiz->get('mark_doubtful')->getString() && $question->isQuestion()) {
        $form['question'][$question->id()]['is_doubtful'] = [
          '#type' => 'checkbox',
          '#title' => $this->t('Doubtful?'),
          '#default_value' => $qra->get('is_doubtful')->getString(),
        ];
      }
    }

    $is_last = $quiz->isLastQuestion();

    $form['navigation']['#type'] = 'actions';

    $form['navigation']['submit_hidden'] = [
      '#weight' => -9999,
      '#type' => 'submit',
      '#value' => $is_last ? $this->t('Finish') : $this->t('Next'),
      '#attributes' => ['style' => 'display: none'],
    ];

    $form['navigation']['timer_expired_finish'] = [
      '#weight' => -9998,
      '#type' => 'submit',
      '#value' => $this->t('Timer Expired Finish'),
      '#submit' => ['::submitFinalize'],
      '#limit_validation_errors' => [['timer_expired_finish']],
      '#attributes' => ['style' => 'display: none'],
    ];

    $form['navigation']['actions'] = [
      '#type' => 'container',
      '#weight' => 5,
    ];

    if ($quiz->get('backwards_navigation')->getString() && ($quiz_session->getCurrentQuestion($quiz) != 1)) {
      // Backwards navigation enabled, and we are looking at not the first
      // question. @todo detect when on the first page.
      $form['navigation']['actions']['return'] = [
        '#type' => 'container',
        '#weight' => 0,
      ];

      $form['navigation']['actions']['return']['back'] = [
        '#weight' => 10,
        '#type' => 'submit',
        '#value' => $this->t('Back'),
        '#submit' => ['::submitBack'],
        '#limit_validation_errors' => [],
      ];
      if ($is_last) {
        $form['navigation']['#last'] = TRUE;
        $form['navigation']['messages'] = [
          '#type' => 'container',
          '#weight' => 0,
        ];
        $form['navigation']['messages']['last_text'] = [
          '#weight' => 0,
          '#markup' => '<p class="quiz-last-text"><em>' . $this->t('This is the last question. Press Finish to deliver your answers') . '</em></p>',
        ];
      }
    }

    $form['navigation']['actions']['proceed'] = [
      '#type' => 'container',
      '#weight' => 20,
    ];

    $form['navigation']['actions']['proceed']['submit'] = [
      '#weight' => 30,
      '#type' => 'submit',
      '#value' => $is_last ? $this->t('Finish') : $this->t('Next'),
      '#ajax' => [],
    ];

    if ($is_last && $quiz->get('backwards_navigation')->getString() && !$quiz->get('repeat_until_correct')->getString()) {
      // Display a confirmation dialogue if this is the last question and a user
      // is able to navigate backwards but not forced to answer correctly.
      $form['#attributes']['class'][] = 'quiz-answer-confirm';
      $form['#attributes']['data-confirm-message'] = $this->t("By proceeding you won't be able to go back and edit your answers.");
      $form['#attached']['library'][] = 'quiz/confirm';
    }
    if ($quiz->get('allow_skipping')->getString()) {
      $form['navigation']['actions']['proceed']['skip'] = [
        '#weight' => 20,
        '#type' => 'submit',
        '#value' => $is_last ? $this->t('Skip and finish') : $this->t('Skip'),
        '#submit' => ['::submitBlank'],
        '#limit_validation_errors' => [],
      ];
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'quiz_question_answering_form';
  }

  /**
   * Submit handler for the question answering form.
   *
   * There is no validation code here, but there may be feedback code for
   * correct feedback.
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $quiz_session = $this->quizSession;

    /** @var \Drupal\quiz\Entity\QuizResult $quiz_result */
    $quiz_result = $form_state->getBuildInfo()['args'][1];
    $feedback_count = 0;
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
    $storage = $this->entityTypeManager->getStorage('quiz');
    /** @var \Drupal\quiz\Entity\Quiz $quiz */
    $quiz = $storage->loadRevision($quiz_result->get('vid')->getString());

    $time_reached = $quiz_result->isTimeReached();
    $layout = $quiz_result->getLayout();

    if ($time_reached) {
      // Too late.
      // @todo move to quiz_question_answering_form_validate(), and then put all
      // the "quiz end" logic in a sharable place. We just need to not fire the
      // logic that saves all the users answers.
      $this->messenger()->addError($this->t('The last answer was not submitted, as the time ran out.'));
    }
    else {
      $submitted = $form_state->getValue('question');

      foreach ($layout as $qra) {
        if (isset($submitted[$qra->get('question_id')->getString()])) {
          // User submitted a response to this question.
          $qqid = $qra->get('question_id')->getString();
          $qra->set('answer_timestamp', $this->time->getRequestTime());

          // Get the unscaled points awarded from the question response
          // implementation and then apply the weighted ratio in this quiz. For
          // example a MCQ question itself may be worth 4 points but worth 10
          // points in this quiz. A score of 2 would mean 5 points being
          // recorded.
          $qra->set('points_awarded', $qra->score($form_state->getValues()['question'][$qqid]) * $qra->getWeightedRatio());

          // Mark question as not skipped, in case it was skipped previously.
          $qra->set('is_skipped', FALSE);

          // Mark as doubtful.
          $qra->set('is_doubtful', !empty($form_state->getValues()['question'][$qqid]['is_doubtful']) ? 1 : 0);

          // Mark correctness.
          $qra->set('is_correct', $qra->isCorrect());

          $qra->save();

          // Does this question type have feedback?
          // We need to track it across pages.
          $feedback_count += $qra->getQuizQuestion()->hasFeedback();

          // Increment the question position.
          $quiz_result->setQuestion((int) $qra->get('number')->getString() + 1);
        }
      }
    }

    // Wat do?
    $next_number = $quiz_session->getCurrentQuestion($quiz);

    if ($time_reached || !isset($layout[$next_number])) {
      // If this is the last question, finalize the quiz.
      $this->submitFinalize($form, $form_state);
    }
    else {
      // No question feedback. Go to next question.
      $form_state->setRedirect('quiz.question.take', [
        'quiz' => $quiz->id(),
        'question_number' => $next_number,
      ]);
    }

    $review_options = $quiz->get('review_options')->get(0);
    if ($review_options && !empty($review_options->getValue()['question']) && array_filter($review_options->getValue()['question']) && $feedback_count) {
      // This page contains questions that can provide feedback, and question
      // feedback is enabled on the quiz.
      $form_state->setRedirect('quiz.question.feedback', [
        'quiz' => $quiz->id(),
        'question_number' => $next_number - 1,
      ]);
      // For ajax_quiz.
      $form_state->set('feedback', TRUE);
    }
  }

  /**
   * Submit action for "leave blank".
   */
  public function submitBlank(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\quiz\Entity\QuizResult $quiz_result */
    $quiz_result = $form_state->getBuildInfo()['args'][1];
    $quiz_session = $this->quizSession;
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
    $storage = $this->entityTypeManager->getStorage('quiz');
    $quiz = $storage->loadRevision($quiz_result->get('vid')->getString());

    if (!empty($form_state->getUserInput()['question'])) {
      // Loop over all question inputs provided, and record them as skipped.
      foreach (array_keys($form_state->getUserInput()['question']) as $qqid) {

        foreach ($quiz_result->getLayout() as $idx => $qra) {
          // Find the blank submitted question in the current layout.
          if ($qra->get('question_id')->getString() == $qqid) {
            // Reset the question and mark it as taken, so restrictions like
            // backwards navigation function correctly.
            $qra->set('is_skipped', TRUE);
            $qra->set('is_correct', NULL);
            $qra->set('points_awarded', FALSE);
            $qra->set('answer_timestamp', $this->time->getRequestTime());
            $qra->save();
          }
        }
        $quiz_result->setQuestion($quiz_session->getCurrentQuestion($quiz) + 1);
      }
    }
    else {
      // Advance to next question, no input here.
      $quiz_result->setQuestion($quiz, $quiz_session->getCurrentQuestion($quiz) + 1);
    }

    // Advance to next question.
    $form_state->setRedirect('quiz.question.take', [
      'quiz' => $quiz->id(),
      'question_number' => $quiz_session->getCurrentQuestion($quiz),
    ]);

    $layout = $quiz_result->getLayout();
    if (!isset($layout[$quiz_session->getCurrentQuestion($quiz)])) {
      // If this is the last question, finalize the quiz.
      $this->submitFinalize($form, $form_state);
    }
  }

  /**
   * Helper function to finalize a quiz attempt.
   *
   * @see quiz_question_answering_form_submit()
   * @see quiz_question_answering_form_submit_blank()
   */
  public function submitFinalize(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\quiz\Entity\QuizResult $quiz_result */
    $quiz_result = $form_state->getBuildInfo()['args'][1];
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
    $storage = $this->entityTypeManager->getStorage('quiz');
    $quiz = $storage->loadRevision($quiz_result->get('vid')->getString());
    $quiz_session = $this->quizSession;

    // No more questions. Score quiz and finalize result.
    $quiz_result->finalize();

    if (empty($quiz->review_options['question']) || !array_filter($quiz->review_options['question']) || empty($form_state['feedback'])) {
      // Only redirect to question results if there is not question feedback.
      // /** @todo D8*/.
      $form_state->setRedirect('entity.quiz_result.canonical', [
        'quiz' => $quiz->id(),
        'quiz_result' => $quiz_result->id(),
      ]);
    }

    // Remove all information about this quiz from the session.
    // @todo but for anon, we might have to keep some so they could access
    // results
    // When quiz is completed we need to make sure that even though the quiz has
    // been removed from the session, that the user can still access the
    // feedback for the last question, THEN go to the results page.
    $quiz_session->setTemporaryResult($quiz_result);
    $quiz_session->removeQuiz($quiz);
  }

  /**
   * Submit handler for "back".
   */
  public function submitBack(&$form, FormStateInterface $form_state): void {
    // Back a question.
    $quiz_session = $this->quizSession;
    $quiz = $form['#quiz'];
    $quiz_result = $quiz_session->getResult($quiz);
    $quiz_result->setQuestion($quiz_session->getCurrentQuestion($quiz) - 1);

    // Handle going back to a question which is in a previous page.
    $layout = $quiz_result->getLayout();
    $question = $layout[$quiz_session->getCurrentQuestion($quiz)];
    if (!empty($question->qqr_pid)) {
      foreach ($layout as $question2) {
        if ($question2->qqr_id == $question->qqr_pid) {
          $quiz_result->setQuestion($question2->get('number')->value);
        }
      }
    }

    $form_state->setRedirect('quiz.question.take', [
      'quiz' => $quiz->id(),
      'question_number' => $quiz_session->getCurrentQuestion($quiz),
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    $quiz_result = $form_state->getBuildInfo()['args'][1];
    $time_reached = $quiz_result->isTimeReached();

    if ($time_reached) {
      // Let's not validate anything, because the input won't get saved
      // in submit either.
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc