Refactors and start of candidate answer relation

This commit is contained in:
2026-03-09 22:00:40 +01:00
parent 6d3402b8e3
commit 45f11f3564
24 changed files with 513 additions and 214 deletions
+5
View File
@@ -6,6 +6,11 @@
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/app</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
+2 -4
View File
@@ -1,5 +1,3 @@
import * as bootstrap from 'bootstrap'
import './bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css'
import './stimulus.js';
import './bootstrap.js'
import './styles/backoffice.scss';
+2 -5
View File
@@ -1,5 +1,2 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
import * as bootstrap from 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
+3 -5
View File
@@ -1,6 +1,4 @@
import './bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css'
import * as bootstrap from 'bootstrap'
import './styles/app.scss'
import './stimulus.js';
import './bootstrap.js'
import './styles/quiz.scss'
+3
View File
@@ -0,0 +1,3 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
-2
View File
@@ -1,2 +0,0 @@
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
+1
View File
@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-go/master/schema/compose-spec.json
services:
php:
build:
+1
View File
@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-go/master/schema/compose-spec.json
# Development environment override
services:
php:
+1
View File
@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-go/master/schema/compose-spec.json
# Production environment override
services:
php:
+1
View File
@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-go/master/schema/compose-spec.json
services:
php:
image: ${IMAGES_PREFIX:-}app-php
+6
View File
@@ -0,0 +1,6 @@
# yaml-language-server: $schema=../../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
when@dev:
maker:
root_namespace: 'Tvdt'
generate_final_classes: true
generate_final_entities: false
-10
View File
@@ -5,12 +5,10 @@ security:
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
tvdt_user_provider:
entity:
class: Tvdt\Entity\User
property: email
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
# Ensure dev tools and static assets are always allowed
@@ -26,14 +24,6 @@ security:
default_target_path: tvdt_backoffice_index
logout:
path: tvdt_login_logout
# where to redirect after logout
# target: tvdt_any_route
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
+1
View File
@@ -8,6 +8,7 @@ when@prod:
ignore_exceptions:
- 'Symfony\Component\ErrorHandler\Error\FatalError'
- 'Symfony\Component\Debug\Exception\FatalErrorException'
- 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'
#
# # If you are using Monolog, you also need this additional configuration to log the errors correctly:
# # https://docs.sentry.io/platforms/php/guides/symfony/integrations/monolog/
+6 -6
View File
@@ -22,23 +22,23 @@ return [
'path' => './assets/backoffice.js',
'entrypoint' => true,
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'bootstrap' => [
'version' => '5.3.6',
'version' => '5.3.8',
],
'@popperjs/core' => [
'version' => '2.11.8',
],
'bootstrap/dist/css/bootstrap.min.css' => [
'version' => '5.3.6',
'version' => '5.3.8',
'type' => 'css',
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
'version' => '8.0.23',
],
];
+113 -1
View File
@@ -16,6 +16,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
use Tvdt\Controller\AbstractController;
use Tvdt\Entity\Candidate;
use Tvdt\Entity\Question;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\Season;
use Tvdt\Exception\ErrorClearingQuizException;
@@ -40,11 +41,122 @@ class QuizController extends AbstractController
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)]
public function index(Season $season, Quiz $quiz): Response
{
return $this->redirectToRoute('tvdt_backoffice_quiz_overview', ['seasonCode' => $season->seasonCode, 'quiz' => $quiz->id]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/overview',
name: 'tvdt_backoffice_quiz_overview',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)]
public function overview(Season $season, Quiz $quiz): Response
{
$fetchedQuiz = $this->quizRepository->fetchWithQuestions($quiz->id);
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $fetchedQuiz,
'activeTab' => 'overview',
'template' => 'backoffice/quiz/tab_overview.html.twig',
]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/result',
name: 'tvdt_backoffice_quiz_result',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)]
public function result(Season $season, Quiz $quiz): Response
{
$fetchedQuiz = $this->quizRepository->fetchWithQuestions($quiz->id);
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $fetchedQuiz,
'result' => $this->quizRepository->getScores($quiz),
'activeTab' => 'result',
'template' => 'backoffice/quiz/tab_result.html.twig',
]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/candidates',
name: 'tvdt_backoffice_quiz_candidates',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)]
public function candidates(Season $season, Quiz $quiz): Response
{
$fetchedQuiz = $this->quizRepository->fetchWithQuestions($quiz->id);
assert($fetchedQuiz->questions->count() > 0);
$firstQuestion = $fetchedQuiz->questions->first();
return $this->redirectToRoute('tvdt_backoffice_quiz_candidates_question', [
'seasonCode' => $season->seasonCode,
'quiz' => $quiz->id,
'question' => $firstQuestion->id,
]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/candidates/{question}',
name: 'tvdt_backoffice_quiz_candidates_question',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
methods: ['GET']
)]
public function candidates_question(Season $season, Quiz $quiz, Question $question): Response
{
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $quiz,
'result' => $this->quizRepository->getScores($quiz),
'question' => $question,
'candidates' => $season->candidates,
'activeTab' => 'candidates',
'template' => 'backoffice/quiz/tab_candidates.html.twig',
]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/candidates/{question}',
name: 'tvdt_backoffice_quiz_candidates_question_save',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
methods: ['POST']
)]
public function saveCandidateAnswers(Season $season, Quiz $quiz, Question $question, Request $request, EntityManagerInterface $em): RedirectResponse
{
$candidateAnswers = $request->request->all('candidate_answer');
// Clear existing candidate-answer associations for this question
foreach ($question->answers as $answer) {
$answer->candidates->clear();
}
// Add new associations
foreach ($candidateAnswers as $candidateId => $answerIds) {
$candidate = $em->getRepository(Candidate::class)->find($candidateId);
foreach ((array) $answerIds as $answerId) {
$answer = $em->getRepository(\Tvdt\Entity\Answer::class)->find($answerId);
if ($answer && $candidate) {
$answer->addCandidate($candidate);
}
}
}
$em->flush();
$this->addFlash('success', $this->translator->trans('Candidate answers saved'));
return $this->redirectToRoute('tvdt_backoffice_quiz_candidates_question', [
'seasonCode' => $season->seasonCode,
'quiz' => $quiz->id,
'question' => $question->id,
]);
}
+6
View File
@@ -57,4 +57,10 @@ class Answer
{
$this->candidates->removeElement($candidate);
}
public function __toString(): string
{
return $this->text;
}
}
+65
View File
@@ -70,6 +70,71 @@ class Question
return 'This question has multiple correct answers';
}
// Check if any answer in the entire quiz has candidate relations
$hasCandidateRelations = false;
foreach ($this->quiz->questions as $quizQuestion) {
foreach ($quizQuestion->answers as $answer) {
if ($answer->candidates->count() > 0) {
$hasCandidateRelations = true;
break 2;
}
}
}
// Only validate candidate-answer relations if at least one exists in the quiz
if ($hasCandidateRelations) {
$seasonCandidates = $this->quiz->season->candidates;
$candidateCounts = [];
// Count how many times each candidate appears in answers
foreach ($this->answers as $answer) {
foreach ($answer->candidates as $candidate) {
$candidateId = $candidate->id->toString();
if (!isset($candidateCounts[$candidateId])) {
$candidateCounts[$candidateId] = ['name' => $candidate->name, 'count' => 0];
}
++$candidateCounts[$candidateId]['count'];
}
}
// Check for missing and duplicate candidates
$missing = [];
$duplicates = [];
foreach ($seasonCandidates as $candidate) {
$candidateId = $candidate->id->toString();
$count = $candidateCounts[$candidateId]['count'] ?? 0;
if (0 === $count) {
$missing[] = $candidate->name;
} elseif ($count > 1) {
$duplicates[] = $candidate->name;
}
}
if (!empty($missing) || !empty($duplicates)) {
$errors = [];
if (!empty($missing)) {
// If all candidates are missing, show a special message
if (\count($missing) === $seasonCandidates->count()) {
$errors[] = 'No candidates assigned to this question';
} else {
$errors[] = 'Missing candidates: '.implode(', ', $missing);
}
}
if (!empty($duplicates)) {
$errors[] = 'Duplicate candidates: '.implode(', ', $duplicates);
}
return implode('. ', $errors);
}
}
return null;
}
public function __toString(): string
{
return $this->question;
}
}
+11
View File
@@ -9,6 +9,7 @@ use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Safe\DateTimeImmutable;
use Safe\Exceptions\DatetimeException;
use Symfony\Component\Uid\Uuid;
use Tvdt\Dto\Result;
use Tvdt\Entity\Quiz;
use Tvdt\Exception\ErrorClearingQuizException;
@@ -105,4 +106,14 @@ class QuizRepository extends ServiceEntityRepository
score: $row['score'],
), $result);
}
public function fetchWithQuestions(Uuid $id): Quiz
{
return $this->getEntityManager()->createQuery(<<<dql
select q, qz, a from Tvdt\Entity\Quiz q
join q.questions qz
join qz.answers a
where q.id = :id
dql)->setParameter('id', $id)->getSingleResult();
}
}
+18 -175
View File
@@ -1,182 +1,25 @@
{% extends 'backoffice/base.html.twig' %}
{% block title %}{{ parent() }}{{ quiz.season.name }}{% endblock %}
{% block title %}{{ parent() }}{{ season.name }}{% endblock %}
{% block body %}
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
<div class="py-2 btn-group" data-controller="bo--quiz">
<a class="btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
href="{{ path('tvdt_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
{% if quiz is same as (season.activeQuiz) %}
<a class="btn btn-secondary"
href="{{ path('tvdt_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
{% endif %}
<button class="btn btn-danger" data-action="click->bo--quiz#clearQuiz">
{{ 'Clear quiz...'|trans }}
</button>
<button class="btn btn-danger" data-action="click->bo--quiz#deleteQuiz">
{{ 'Delete Quiz...'|trans }}
</button>
</div>
{% set tabs = [
{id: 'overview', label: 'Overview'|trans, route: 'tvdt_backoffice_quiz_overview'},
{id: 'result', label: 'Results & Elimination'|trans, route: 'tvdt_backoffice_quiz_result'},
{id: 'candidates', label: 'Candidates'|trans, route: 'tvdt_backoffice_quiz_candidates'},
] %}
<div id="questions">
<h4 class="py-2">{{ 'Questions'|trans }}</h4>
<div class="accordion">
{%~ for question in quiz.questions ~%}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#question-{{ loop.index0 }}"
aria-controls="question-{{ loop.index0 }}">
{% set questionErrors = question.getErrors %}
{%~ if questionErrors -%}
<span data-bs-toggle="tooltip"
title="{{ questionErrors }}"
class="badge text-bg-danger rounded-pill me-2">!</span>
{% endif %}
{{~ loop.index -}}. {{ question.question -}}
</button>
</h2>
<div id="question-{{ loop.index0 }}"
class="accordion-collapse collapse">
<div class="accordion-body">
<ul>
{%~ for answer in question.answers %}
<li{% if answer.isRightAnswer %} class="text-decoration-underline"{% endif %}>{{ answer.text -}}</li>
{%~ else %}
{{ 'There are no answers for this question'|trans -}}
{%~ endfor %}
</ul>
</div>
</div>
</div>
{% else %}
{{ 'EMPTY'|trans }}
{% endfor %}
</div>
</div>
<div class="scores">
<h4 class="py-2">{{ 'Score'|trans }}</h4>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group btn-group-lg me-2">
{# <a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a> #}
<a href="{{ path('tvdt_prepare_elimination', {seasonCode: season.seasonCode, quiz: quiz.id}) }}"
class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a>
{%~ if not quiz.eliminations.empty %}
<button class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown">{{ 'Load Prepared Elimination'|trans }}</button>
<ul class="dropdown-menu">
{%~ for elimination in quiz.eliminations %}
<li><a class="dropdown-item"
href="{{ path('tvdt_prepare_elimination_view', {elimination: elimination.id}) }}">{{ elimination.created|format_datetime() }}</a>
</li>
{%~ endfor %}
</ul>
{% endif %}
</div>
</div>
<p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{{ 'Candidate'|trans }}</th>
<th style="width: 15%" scope="col">{{ 'Correct Answers'|trans }}</th>
<th style="width: 20%" scope="col">{{ 'Corrections'|trans }}</th>
<th style="width: 20%" scope="col">{{ 'Penalty'|trans }}</th>
<th style="width: 10%" scope="col">{{ 'Score'|trans }}</th>
<th style="width: 20%" scope="col">{{ 'Time'|trans }}</th>
</tr>
</thead>
<tbody>
{%~ for candidate in result ~%}
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
<td>{{ candidate.name }}</td>
<td>{{ candidate.correct|default('0') }}</td>
<td>
<form method="post"
action="{{ path('tvdt_backoffice_modify_correction', {quiz: quiz.id, candidate: candidate.id}) }}">
<div class="row">
<div class="col-8">
<input class="form-control form-control-sm" type="number"
value="{{ candidate.corrections }}" step="0.5"
name="corrections">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary" type="submit">{{ 'Save'|trans }}</button>
</div>
</div>
</form>
</td>
<td>
<form method="post"
action="{{ path('tvdt_backoffice_modify_penalty', {quiz: quiz.id, candidate: candidate.id}) }}">
<div class="row">
<div class="col-8">
<input class="form-control form-control-sm" type="number"
value="{{ candidate.penaltySeconds }}" step="1"
name="penalty">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary" type="submit">{{ 'Save'|trans }}</button>
</div>
</div>
</form>
</td>
<td>{{ candidate.score|default('x') }}</td>
<td>{{ candidate.time.format('%i:%S') }}</td>
</tr>
{% else %}
<tr>
<td colspan="5">{{ 'No results'|trans }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Modal Clear #}
<div class="modal fade" id="clearQuizModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">{{ 'Please Confirm'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ 'Are you sure you want to clear all the results? This will also delete al the eliminations.'|trans }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
<a href="{{ path('tvdt_backoffice_quiz_clear', {quiz: quiz.id}) }}"
class="btn btn-danger">{{ 'Yes'|trans }}</a>
</div>
</div>
</div>
</div>
{# Modal Delete #}
<div class="modal fade" id="deleteQuizModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">{{ 'Please Confirm'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ 'Are you sure you want to delete this quiz?'|trans }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
<a href="{{ path('tvdt_backoffice_quiz_delete', {quiz: quiz.id}) }}"
class="btn btn-danger">{{ 'Yes'|trans }}</a>
</div>
</div>
</div>
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ season.name }} - {{ quiz.name }}</h2>
<ul class="nav nav-tabs pt-2">
{% for tab in tabs %}
<li class="nav-item">
<a class="nav-link{{ activeTab == tab.id ? ' active' }}" href="{{ path(tab.route, {seasonCode: season.seasonCode, quiz: quiz.id}) }}">
{{ tab.label }}
</a>
</li>
{% endfor %}
</ul>
<div class="pt-3">
{{ include(template) }}
</div>
{% endblock %}
@@ -0,0 +1,65 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
{% set questions = quiz.questions %}
{% set currentIndex = null %}
{% for index, q in questions %}
{% if q.id.toString == question.id.toString %}
{% set currentIndex = index %}
{% endif %}
{% endfor %}
{% if currentIndex > 0 %}
{% set prevQuestion = questions[currentIndex - 1] %}
<a href="{{ path('tvdt_backoffice_quiz_candidates_question', {seasonCode: season.seasonCode, quiz: quiz.id, question: prevQuestion.id}) }}"
class="btn btn-secondary">
{{ 'Previous'|trans }}
</a>
{% endif %}
</div>
<h3 class="mb-0">{{ currentIndex + 1 }}. {{ question }}</h3>
<div>
{% if currentIndex is not null and currentIndex < (questions|length - 1) %}
{% set nextQuestion = questions[currentIndex + 1] %}
<a href="{{ path('tvdt_backoffice_quiz_candidates_question', {seasonCode: season.seasonCode, quiz: quiz.id, question: nextQuestion.id}) }}"
class="btn btn-secondary">
{{ 'Next'|trans }}
</a>
{% endif %}
</div>
</div>
<form method="post">
<table class="table table-hover table-striped">
<thead>
<tr>
<th scope="col">{{ 'Candidate'|trans }}</th>
{% for answer in question.answers %}
<th>{{ answer }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for candidate in candidates %}
<tr>
<th scope="row">{{ candidate.name }}</th>
{% for answer in question.answers %}
<td>
<input type="checkbox"
id="candidate_{{ candidate.id }}_answer_{{ answer.id }}"
name="candidate_answer[{{ candidate.id }}][]"
value="{{ answer.id }}"
class="form-check-input"
{{ answer.candidates.contains(candidate) ? 'checked' : '' }}>
<label for="candidate_{{ candidate.id }}_answer_{{ answer.id }}" class="visually-hidden">
{{ candidate.name }} - {{ answer }}
</label>
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit" class="btn btn-primary">{{ 'Save'|trans }}</button>
</form>
@@ -0,0 +1,107 @@
<h4 class="py-2">Quick actions</h4>
<div class="py-2 btn-group" data-controller="bo--quiz">
{% if quiz is same as (season.activeQuiz) %}
<a class="btn btn-secondary"
href="{{ path('tvdt_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
{% else %}
<a class="btn btn-primary"
href="{{ path('tvdt_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
{% endif %}
<button class="btn btn-danger" data-action="click->bo--quiz#clearQuiz">
{{ 'Clear Quiz...'|trans }}
</button>
<button class="btn btn-danger" data-action="click->bo--quiz#deleteQuiz">
{{ 'Delete Quiz...'|trans }}
</button>
</div>
<div id="questions">
<h4 class="py-2">{{ 'Questions'|trans }}</h4>
<div class="accordion">
{%~ for question in quiz.questions ~%}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#question-{{ loop.index0 }}"
aria-controls="question-{{ loop.index0 }}">
{% set questionErrors = question.getErrors %}
{%~ if questionErrors -%}
<span data-bs-toggle="tooltip"
title="{{ questionErrors }}"
class="badge text-bg-danger rounded-pill me-2">!</span>
{% endif %}
{{~ loop.index -}}. {{ question.question -}}
</button>
</h2>
<div id="question-{{ loop.index0 }}"
class="accordion-collapse collapse">
<div class="accordion-body">
<ul>
{%~ for answer in question.answers %}
<li{% if answer.isRightAnswer %} class="text-decoration-underline"{% endif %}>
{{ answer.text }}
{% if answer.candidates|length > 0 %}
<small class="text-muted">
({{ answer.candidates|map(c => c.name)|join(', ') }})
</small>
{% endif %}
</li>
{%~ else %}
{{ 'There are no answers for this question'|trans -}}
{%~ endfor %}
</ul>
</div>
</div>
</div>
{% else %}
{{ 'EMPTY'|trans }}
{% endfor %}
</div>
</div>
{# Modal Clear #}
<div class="modal fade" id="clearQuizModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">{{ 'Please Confirm'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ 'Are you sure you want to clear all the results? This will also delete al the eliminations.'|trans }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
<a href="{{ path('tvdt_backoffice_quiz_clear', {quiz: quiz.id}) }}"
class="btn btn-danger">{{ 'Yes'|trans }}</a>
</div>
</div>
</div>
</div>
{# Modal Delete #}
<div class="modal fade" id="deleteQuizModal" data-bs-backdrop="static"
tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">{{ 'Please Confirm'|trans }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ 'Are you sure you want to delete this quiz?'|trans }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
<a href="{{ path('tvdt_backoffice_quiz_delete', {quiz: quiz.id}) }}"
class="btn btn-danger">{{ 'Yes'|trans }}</a>
</div>
</div>
</div>
</div>
@@ -0,0 +1,78 @@
<div class="scores">
<h4 class="py-2">{{ 'Score'|trans }}</h4>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group btn-group-lg me-2">
{# <a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a> #}
<a href="{{ path('tvdt_prepare_elimination', {seasonCode: season.seasonCode, quiz: quiz.id}) }}"
class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a>
{%~ if not quiz.eliminations.empty %}
<button class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown">{{ 'Load Prepared Elimination'|trans }}</button>
<ul class="dropdown-menu">
{%~ for elimination in quiz.eliminations %}
<li><a class="dropdown-item"
href="{{ path('tvdt_prepare_elimination_view', {elimination: elimination.id}) }}">{{ elimination.created|format_datetime() }}</a>
</li>
{%~ endfor %}
</ul>
{% endif %}
</div>
</div>
<p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{{ 'Candidate'|trans }}</th>
<th style="width: 15%" scope="col">{{ 'Correct Answers'|trans }}</th>
<th style="width: 20%" scope="col">{{ 'Corrections'|trans }}</th>
<th style="width: 20%" scope="col">{{ 'Penalty'|trans }}</th>
<th style="width: 10%" scope="col">{{ 'Score'|trans }}</th>
<th style="width: 20%" scope="col">{{ 'Time'|trans }}</th>
</tr>
</thead>
<tbody>
{%~ for candidate in result ~%}
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
<td>{{ candidate.name }}</td>
<td>{{ candidate.correct|default('0') }}</td>
<td>
<form method="post"
action="{{ path('tvdt_backoffice_modify_correction', {quiz: quiz.id, candidate: candidate.id}) }}">
<div class="row">
<div class="col-8">
<input class="form-control form-control-sm" type="number"
value="{{ candidate.corrections }}" step="0.5"
name="corrections">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary" type="submit">{{ 'Save'|trans }}</button>
</div>
</div>
</form>
</td>
<td>
<form method="post"
action="{{ path('tvdt_backoffice_modify_penalty', {quiz: quiz.id, candidate: candidate.id}) }}">
<div class="row">
<div class="col-8">
<input class="form-control form-control-sm" type="number"
value="{{ candidate.penaltySeconds }}" step="1"
name="penalty">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary" type="submit">{{ 'Save'|trans }}</button>
</div>
</div>
</form>
</td>
<td>{{ candidate.score|default('x') }}</td>
<td>{{ candidate.time.format('%i:%S') }}</td>
</tr>
{% else %}
<tr>
<td colspan="5">{{ 'No results'|trans }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
+18 -6
View File
@@ -57,8 +57,8 @@
<source>Candidates</source>
<target>Kandidaten</target>
</trans-unit>
<trans-unit id="FNY513f" resname="Clear quiz...">
<source>Clear quiz...</source>
<trans-unit id="J1c2y63" resname="Clear Quiz...">
<source>Clear Quiz...</source>
<target>Test leegmaken...</target>
</trans-unit>
<trans-unit id="sFpB4C2" resname="Correct Answers">
@@ -149,14 +149,14 @@
<source>Name</source>
<target>Naam</target>
</trans-unit>
<trans-unit id="wd1MvZW" resname="No">
<source>No</source>
<target>Nee</target>
</trans-unit>
<trans-unit id="gefhnBC" resname="Next">
<source>Next</source>
<target>Volgende</target>
</trans-unit>
<trans-unit id="wd1MvZW" resname="No">
<source>No</source>
<target>Nee</target>
</trans-unit>
<trans-unit id="nOHriCl" resname="No active quiz">
<source>No active quiz</source>
<target>Geen actieve test</target>
@@ -169,6 +169,10 @@
<source>Number of dropouts:</source>
<target>Aantal afvallers:</target>
</trans-unit>
<trans-unit id="HmgPmMV" resname="Overview">
<source>Overview</source>
<target>Overzicht</target>
</trans-unit>
<trans-unit id="PywqOf4" resname="Owner(s)">
<source>Owner(s)</source>
<target>Eigena(a)r(en)</target>
@@ -177,6 +181,10 @@
<source>Password</source>
<target>Wachtwoord</target>
</trans-unit>
<trans-unit id="1ne1Zlc" resname="Penalty">
<source>Penalty</source>
<target>Straf</target>
</trans-unit>
<trans-unit id="VbgD9L8" resname="Please Confirm">
<source>Please Confirm</source>
<target>Bevestig Alsjeblieft</target>
@@ -249,6 +257,10 @@
<source>Repeat Password</source>
<target>Herhaal wachtwoord</target>
</trans-unit>
<trans-unit id="7UvBPrb" resname="Results &amp; Elimination">
<source>Results &amp; Elimination</source>
<target><![CDATA[Resultaat & Eliminatie]]></target>
</trans-unit>
<trans-unit id="z9OKodR" resname="Save">
<source>Save</source>
<target>Opslaan</target>