mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-06 07:30:17 +02:00
* feat: add question bank management, quiz finalization, and related backend/frontend functionality * chore: add symfony/object-mapper dependency and fix Finalized translation * feat: address PR review comments — unassign/sync bank questions, blank quiz creation, deactivate redirect, remove duplicate tab titles - Use Symfony ObjectMapper for BankQuestion/BankAnswer → Question/Answer copy (#[Map(if: false)] on id, season, etc.) - Track created Question on BankQuestionUsage (nullable FK, onDelete: SET NULL) for unassign/sync support - Add unassign route: removes the Question copy + usage record - Add sync route: pushes bank question edits to a finalized-not-started quiz copy - Auto-sync non-finalized quiz copies on bank question edit; flash warning for finalized-not-started - Add blank quiz creation (no XLSX required) with new route + template - Deactivate quiz button now stays on the quiz overview page (redirect_quiz hidden field) - Remove duplicate h4 titles below the tab bar on all season tabs - Add migration for bank_question_usage.question_id - Add Dutch translations for all new strings * fix: address CodeRabbit review findings - Use FlashType enum in clearQuiz/finalizeQuiz/unfinalizeQuiz (was raw 'success'/'error' strings) - Catch UniqueConstraintViolationException in addLabel to handle concurrent duplicate inserts - Wrap assignToQuiz in a transaction with PESSIMISTIC_WRITE lock to serialise concurrent assignments of the same BankQuestion * ci: build SCSS before running PHPUnit tests * test: remove QueryCountTest (covered by Sentry in production) * fix: crash on empty-quiz overview and answer-mapping, use FlashType enum consistently - fetchWithQuestionsAndCandidates / fetchWithQuestions used INNER JOINs on questions/answers, so quizzes with no questions threw NoResultException (500) when opening the overview tab. Switched to LEFT JOINs. - answerMapping bare \assert() replaced with a proper flash + redirect when the quiz has no questions, instead of crashing with AssertionError. - Three raw 'success' flash strings in QuizController replaced with FlashType::Success. - Added Dutch translation for "This quiz has no questions yet". - Two new tests: empty-quiz overview loads (200), answer-mapping redirects with flash. * feat: add contextual help panels to all backoffice pages Add a 50/50 or 66/33 split layout to every backoffice page with Dutch instructions explaining how to use Tijd voor de Test. Content covers the overall workflow, quiz finalize/activate flow, and both candidate participation methods (own device vs. shared laptop). Help text lives in dedicated partials under templates/backoffice/help/ and is loaded via Twig include(), keeping page templates clean. All strings use a separate 'instructions' translation domain (instructions+intl-icu.nl.xliff) isolated from the main messages domain. Also updates 'Finalize'-related Dutch translations to use 'Afronden' and adds tooltips to the finalize/unfinalize buttons. * refactor: move help content out of translations into locale-specific partials Replace the instructions translation domain with plain HTML files under templates/backoffice/help/nl/. Each help/*.html.twig is now a locale dispatch shim that tries the current locale first and falls back to nl, so adding English (or any other language) is simply a matter of creating a help/en/ directory with the translated files — no code changes needed. Removes instructions+intl-icu.nl.xliff. * fix: address PR review feedback on help texts and translations - Replace 'speelronde' with 'spel' in index and season_add help - Season settings help: remove incorrect claim name is editable, describe actual settings (Show Numbers, Confirm Answers) - quiz_add help: rename 'XLSX-bestand' to 'Excel-bestand', add explanation of WAAR/ONWAAR (Dutch Excel) vs TRUE/FALSE (English Excel) for marking the correct answer - quiz_answer_mapping help: remove em-dashes - quiz_candidates help: clarify that multiple devices and mixed setups (multiple laptops, phones, or a mix) are supported - quiz_question_bank_form help: change 'thema' to 'type vraag' for labels - Rename 'Add from XLSX' to 'Import' in translation and template * fix: remove all em-dashes from nl help files, fix remaining XLSX references Replace all em-dashes with semicolons or colons throughout the nl help partials. Also replace remaining 'XLSX-bestand' with 'Excel-bestand' in season_tests and index, and also also fix the em-dash that was still on the same line as the 'speelronde -> spel' fix in index. * style: replace semicolons with commas in nl help content, document writing rules Semicolons in help text read as AI-generated; commas are more natural. Added writing style rules to CLAUDE.md to prevent recurrence. * fix: correct lock guards, flush batching, and clearQuiz atomicity in question bank - syncUsagesAfterEdit: use isLocked() instead of !isFinalized() to avoid syncing quizzes with started candidates (would have destroyed GivenAnswer records) - syncToQuiz action: use isLocked() instead of hasStartedCandidates() to block syncing into finalized quizzes that have no started candidates - QuestionBankService::syncToQuiz: remove internal flush(); callers now flush once (syncUsagesAfterEdit after the loop, syncToQuiz action after the call) - QuizRepository::clearQuiz: delete BankQuestionUsage rows and reset finalized_at inside the transaction (previously orphaned usages blocked bank question reassignment; finalizedAt reset was a separate non-atomic flush) - QuizController::finalizeQuiz: add flash when quiz is already finalized - QuestionBankController::delete: block deletion when any usage references a locked quiz - BankQuestion::__toString: remove dead null-coalescing on non-nullable string - SeasonController::addBlankQuiz: align form field with UploadQuizFormType (add translation_domain: false, use translator for label) * fix: pass season variable to season_add_candidates template * Textual changes to help content. * Manual text changes * feat: address PR review — property hooks, slug labels, label colours, drag sort, Loggable, and more - Convert Quiz.isFinalized/isLocked/hasStartedCandidates and BankQuestion.isUsed/canBeAssigned to PHP 8.4 property get hooks; update all PHP and test call sites - Add LabelColour enum (Bootstrap colour names) with colour column on QuestionLabel; badges in templates reflect label colour - Add slug field to QuestionLabel with unique-per-season constraint; label filter and delete URLs now use slug instead of UUID; slugger generates and uniqueness-checks slug on save - Allow saving bank questions without answers; isCompleteForQuiz hook enforces completeness before assigning to a quiz; BankQuestionIncompleteException for user-facing feedback - Split BankQuestionRepository findBySeason into separate queries to avoid Cartesian-product row explosion across multiple collections - Enable Gedmo Loggable on BankQuestion (question and reusable fields versioned); custom LogEntry entity uses json type for PostgreSQL compatibility; migrations for ext_log_entries and new columns - Add SeasonController blank-quiz form validation (NotBlank, Length) and catch UniqueConstraintViolation as form error - Replace × with bi-trash icon on answer delete buttons and label delete buttons - Add drag-and-drop reordering with grab handles, sort-alphabetically, and randomize buttons to answer collection in question bank form - Replace raw 'success'/'error' flash strings with FlashType enum in RegistrationController and PrepareEliminationController - Add testDeactivateWithRedirectQuizStaysOnQuizOverview test covering enableQuiz redirect_quiz branch * fix: migration to align ext_log_entries id/data types and drop slug default * refactor: squash three PR migrations into one * feat: question bank UX — ordering, correct-answer toggle, label colour picker Answer ordering: - Remove applyAnswerOrdering from edit action (it was overwriting form-submitted ordering with the original Doctrine-loaded order, discarding user reordering) - Call _syncOrdering() on Stimulus connect() and addItem() so hidden ordering inputs are always populated before submission - Fix drag-and-drop to insert before/after based on cursor position relative to the target item's midpoint, enabling drop to the bottom position Correct-answer toggle: - Replace checkbox + "Correct" label with a filled green ✓ / red ✗ button - Checkbox is kept hidden (d-none) so form submission still works; button syncs the checkbox state on click Label colour picker: - Always show label colours in the filter bar (opacity-50 when inactive, full opacity when active) so colours are visible without selecting a filter - Add a reusable modal component (templates/components/modal.html.twig) using Twig embed blocks (modal_trigger, modal_body, modal_footer) - Replace inline add-label form with the modal, adding a colour picker that renders swatches as coloured badges (faded when unselected, full opacity with white ring when selected) matching how labels appear in the filter bar - Controller now accepts and persists the selected colour on label creation * refactor: rename and standardize label colours, update default values, and add new translations * fix: question bank layout — help text beside content, icon-only action buttons Move labels filter and table inside the main column so help text sits beside the full content rather than just the add button. Convert Edit, Delete, and Assign buttons to icon-only btn-group to save row space. * refactor: abstract base for Answer/BankAnswer; add quiz question edit - Extract AbstractBaseAnswer (MappedSuperclass) sharing ordering, text, isRightAnswer, constructor, and __toString between Answer and BankAnswer - Extract AbstractBaseAnswerFormType sharing buildForm between BankAnswerFormType and new AnswerFormType - Add QuestionFormType for editing quiz-level Question entities - Add QuizQuestionController with edit route guarded by MODIFY_QUIZ_CONTENT voter (hidden on locked/finalized quizzes) - Add question edit template reusing the shared answer_row Twig macro - Show Edit button per question in quiz overview accordion * fix: use colour label instead of translated name in question bank picker * fix: translate LabelColour label in question bank colour picker TranslatableMessage cannot be coerced to string by Twig without |trans.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/index.html.twig',
|
||||
'backoffice/help/nl/index.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,10 @@
|
||||
<h6>Aan de slag</h6>
|
||||
<p>Elk seizoen groepeert één spel met alle bijbehorende testen en kandidaten. De seizoenscode is de link die kandidaten gebruiken om een test te starten, en is alleen actief als er een actieve test is.</p>
|
||||
<h6>Globale werkwijze</h6>
|
||||
<ol>
|
||||
<li><strong>Seizoen aanmaken</strong> en kandidaten toevoegen</li>
|
||||
<li><strong>Test aanmaken</strong> via Excel of de vragenbank</li>
|
||||
<li>Test <strong>afronden</strong> en <strong>activeren</strong></li>
|
||||
<li>Kandidaten laten <strong>deelnemen</strong> (eigen apparaat of gedeelde laptop)</li>
|
||||
<li><strong>Resultaten</strong> bekijken en eliminatie starten</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Eliminatie voorbereiden</h6>
|
||||
<p>Kies voor elke kandidaat een kleur: <strong>groen</strong> betekent veilig, <strong>rood</strong> betekent geëlimineerd.</p>
|
||||
<p>Gebruik <strong>Opslaan en starten</strong> om de eliminatie direct af te spelen, of sla eerst op en start later via het tabblad Resultaten.</p>
|
||||
@@ -0,0 +1,8 @@
|
||||
<h6>Test importeren via Excel</h6>
|
||||
<p>Upload een Excel-bestand met de vragen voor deze test. Na het uploaden kun je de vragen bekijken, controleren en de test afronden.</p>
|
||||
<h6>Verwacht formaat</h6>
|
||||
<ul>
|
||||
<li>Eerste kolom: de vraagtekst</li>
|
||||
<li>Volgende kolommen: de antwoordopties</li>
|
||||
<li>Markeer het juiste antwoord met <strong>WAAR</strong> (Nederlandstalige Excel) of <strong>TRUE</strong> (Engelstalige Excel), alle andere antwoorden zet je op ONWAAR/FALSE</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Lege test aanmaken</h6>
|
||||
<p>Maak een lege test aan en voeg vragen toe vanuit de vragenbank. Handig als je vragen hergebruikt of ze van tevoren in de bank hebt klaargezet.</p>
|
||||
<p>Na het aanmaken open je de test, voeg je vragen toe via het tabblad Overzicht en ronde je de test af voordat je hem activeert.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Antwoorden invullen</h6>
|
||||
<p>Gebruik dit formulier om antwoorden aan kandidaten toe te wijzen. Op deze manier kunnen er statistieken gemaakt worden hoe verdacht kandidaten zijn.</p>
|
||||
<p>Navigeer met de knoppen Vorige en Volgende tussen vragen. Vink per kandidaat het gegeven antwoord aan en sla op.</p>
|
||||
@@ -0,0 +1,6 @@
|
||||
<h6>Kandidaten laten deelnemen</h6>
|
||||
<p>Kandidaten kunnen de test invullen via hun eigen apparaat, een of meerdere gedeelde laptops, of een mix daarvan.</p>
|
||||
<p><strong>Eigen apparaat:</strong> Deel de seizoenscode. Elke kandidaat bezoekt de site op zijn of haar telefoon of laptop, voert de eigen naam in en start de test.</p>
|
||||
<p><strong>Gedeelde laptop(s):</strong> Open de naamsinvoerpagina van tevoren op een of meerdere laptops. Elke kandidaat typt zijn of haar naam en start. Na afloop kan de volgende kandidaat hetzelfde doen op dezelfde of een andere laptop.</p>
|
||||
<h6>Status</h6>
|
||||
<p>Deactiveer een kandidaat als deze de test niet hoeft te maken, bijvoorbeeld na eerder uitgeschakeld zijn. Deactivering is per test en heeft geen invloed op andere testen.</p>
|
||||
@@ -0,0 +1,5 @@
|
||||
<h6>Overzicht & afronden</h6>
|
||||
<p>Vragen met een rode markering in de lijst hiernaast bevatten een fout. Herstel deze vóór het afronden.</p>
|
||||
<p><strong>Afronden</strong> vergrendelt de test voor bewerking en maakt hem klaar voor kandidaten. Daarna kun je hem activeren.</p>
|
||||
<p><strong>Activeren</strong> stelt de test beschikbaar aan kandidaten. Er kan maar één test tegelijk actief zijn, activeer de volgende pas als iedereen de huidige heeft afgerond.</p>
|
||||
<p><strong>Test wissen</strong> verwijdert alle gegeven antwoorden en heft het afronden op, zodat je de test opnieuw kunt bewerken en uitvoeren.</p>
|
||||
@@ -0,0 +1,4 @@
|
||||
<h6>Vraag toevoegen</h6>
|
||||
<p>Voer de vraag in en voeg minimaal twee antwoordopties toe. Markeer precies één antwoord als correct.</p>
|
||||
<p>Gebruik labels om vragen te organiseren in de vragenbank, bijvoorbeeld per aflevering of type vraag.</p>
|
||||
<p>Markeer een vraag als <em>herbruikbaar</em> als deze in meerdere testen mag voorkomen, anders kan een vraag maar aan één test worden gekoppeld.</p>
|
||||
@@ -0,0 +1,5 @@
|
||||
<h6>Resultaten</h6>
|
||||
<p>De tabel toont het eindresultaat per kandidaat gesorteerd op score. Rode rijen zijn de kandidaten met de laagste score die risico lopen op eliminatie.</p>
|
||||
<p><strong>Jokers</strong> voeg je toe voor goede of foute vragen (halve punten zijn mogelijk).</p>
|
||||
<p><strong>Straftijd</strong> is tijdstraf in seconden en wordt meegewogen bij gelijke score. Let op: Een positief getal is een straf en een negatief getal is een bonus.</p>
|
||||
<p>Via <strong>Eliminatie voorbereiden</strong> stel je de schermkleuren handmatig in en start je de eliminatie.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Nieuw seizoen</h6>
|
||||
<p>Een seizoen groepeert alle testen en kandidaten voor één spel. Geef het seizoen een herkenbare naam, de seizoenscode wordt automatisch gegenereerd.</p>
|
||||
<p>Na het aanmaken voeg je kandidaten toe en maak je testen aan via de seizoenpagina.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Kandidaten toevoegen</h6>
|
||||
<p>Voer één naam per regel in. Dit zijn de spelers die deelnemen aan dit seizoen.</p>
|
||||
<p>Je kunt later altijd nog kandidaten toevoegen via het tabblad Kandidaten. Gebruik dezelfde schrijfwijze van namen die je in het spel gebruikt.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Kandidaten</h6>
|
||||
<p>Dit zijn de spelers van dit seizoen. Voeg alle deelnemers toe voordat je de eerste test start, kandidaten worden automatisch aan nieuwe testen gekoppeld.</p>
|
||||
<p>Namen zijn vrij in te voeren, gebruik dezelfde schrijfwijze die je in het spel gebruikt.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Vragenbank</h6>
|
||||
<p>De vragenbank is een bibliotheek met vragen die aan meerdere testen kunnen worden gekoppeld. Markeer een vraag als <em>herbruikbaar</em> als deze in meerdere testen mag voorkomen (bijv. "Wie is de Mol?").</p>
|
||||
<p>Na het bewerken van een vraag in de bank worden testen die de vraag al bevatten <strong>niet</strong> automatisch bijgewerkt, gebruik de synchronisatieknop (↻) naast een test om de meest recente versie door te zetten.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h6>Seizoensinstellingen</h6>
|
||||
<p>Pas hier de weergave-instellingen van dit seizoen aan.</p>
|
||||
<p><strong>Nummers tonen:</strong> toont vraagnummers tijdens de test. <strong>Antwoord bevestigen:</strong> vraagt kandidaten om hun antwoord te bevestigen voordat ze doorgaan.</p>
|
||||
@@ -0,0 +1,11 @@
|
||||
<h6>Testen beheren</h6>
|
||||
<p>Voeg een test toe vanuit een Excel-bestand of maak een lege test aan en vul deze via de vragenbank. Open daarna de test om hem te bekijken en af te ronden.</p>
|
||||
<p>Een test moet eerst <strong>afgerond</strong> zijn voordat je hem kunt activeren. Slechts één test kan tegelijk actief zijn, namelijk de test die kandidaten op dat moment kunnen invullen.</p>
|
||||
<h6>Volgorde van werken</h6>
|
||||
<ol>
|
||||
<li>Test aanmaken (Excel of leeg)</li>
|
||||
<li>Vragen controleren en test afronden</li>
|
||||
<li>Test activeren</li>
|
||||
<li>Kandidaten laten deelnemen</li>
|
||||
<li>Resultaten bekijken en eliminatie voorbereiden</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/prepare_elimination.html.twig',
|
||||
'backoffice/help/nl/prepare_elimination.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_add.html.twig',
|
||||
'backoffice/help/nl/quiz_add.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_add_blank.html.twig',
|
||||
'backoffice/help/nl/quiz_add_blank.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_answer_mapping.html.twig',
|
||||
'backoffice/help/nl/quiz_answer_mapping.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_candidates.html.twig',
|
||||
'backoffice/help/nl/quiz_candidates.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_overview.html.twig',
|
||||
'backoffice/help/nl/quiz_overview.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_question_bank_form.html.twig',
|
||||
'backoffice/help/nl/quiz_question_bank_form.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/quiz_result.html.twig',
|
||||
'backoffice/help/nl/quiz_result.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/season_add.html.twig',
|
||||
'backoffice/help/nl/season_add.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/season_add_candidates.html.twig',
|
||||
'backoffice/help/nl/season_add_candidates.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/season_candidates.html.twig',
|
||||
'backoffice/help/nl/season_candidates.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/season_question_bank.html.twig',
|
||||
'backoffice/help/nl/season_question_bank.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/season_settings.html.twig',
|
||||
'backoffice/help/nl/season_settings.html.twig',
|
||||
]) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ include([
|
||||
'backoffice/help/' ~ app.request.locale ~ '/season_tests.html.twig',
|
||||
'backoffice/help/nl/season_tests.html.twig',
|
||||
]) }}
|
||||
@@ -11,53 +11,60 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex flex-row align-items-center mb-3">
|
||||
<h2 class="mb-0 pe-2">
|
||||
{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}
|
||||
</h2>
|
||||
<a class="link" href="{{ path('tvdt_backoffice_season_add') }}">
|
||||
{{ 'Add'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
{% if seasons %}
|
||||
<table class="table table-hover mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<th scope="col">{{ 'Owner(s)'|trans }}</th>
|
||||
{% endif %}
|
||||
<th scope="col">{{ 'Name'|trans }}</th>
|
||||
<th scope="col">{{ 'Active Quiz'|trans }}</th>
|
||||
<th scope="col">{{ 'Season Code'|trans }}</th>
|
||||
<th scope="col">{{ 'Manage'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for season in seasons %}
|
||||
<tr class="align-middle">
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
|
||||
{% endif %}
|
||||
<td>{{ season.name }}</td>
|
||||
<td>
|
||||
{% if season.activeQuiz %}
|
||||
{{ season.activeQuiz.name }}
|
||||
{% else %}
|
||||
{{ 'No active quiz'|trans }}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-12">
|
||||
<div class="d-flex flex-row align-items-center mb-3">
|
||||
<h2 class="mb-0 pe-2">
|
||||
{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}
|
||||
</h2>
|
||||
<a class="link" href="{{ path('tvdt_backoffice_season_add') }}">
|
||||
{{ 'Add'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
{% if seasons %}
|
||||
<table class="table table-hover mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<th scope="col">{{ 'Owner(s)'|trans }}</th>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a {% if season.activeQuiz %}href="{{ path('tvdt_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
|
||||
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ 'You have no seasons yet.'|trans }}
|
||||
{% endif %}
|
||||
<th scope="col">{{ 'Name'|trans }}</th>
|
||||
<th scope="col">{{ 'Active Quiz'|trans }}</th>
|
||||
<th scope="col">{{ 'Season Code'|trans }}</th>
|
||||
<th scope="col">{{ 'Manage'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for season in seasons %}
|
||||
<tr class="align-middle">
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
|
||||
{% endif %}
|
||||
<td>{{ season.name }}</td>
|
||||
<td>
|
||||
{% if season.activeQuiz %}
|
||||
{{ season.activeQuiz.name }}
|
||||
{% else %}
|
||||
{{ 'No active quiz'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a {% if season.activeQuiz %}href="{{ path('tvdt_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
|
||||
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ 'You have no seasons yet.'|trans }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
{{ include('backoffice/help/index.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{% macro answer_row(answerForm) %}
|
||||
<div class="d-flex align-items-center gap-2 mb-2" data-collection-item>
|
||||
{{ form_widget(answerForm.ordering) }}
|
||||
<span class="text-muted" data-drag-handle style="cursor: grab" title="{{ 'Drag to reorder'|trans }}"><i class="bi bi-grip-vertical"></i></span>
|
||||
<div class="flex-grow-1">{{ form_widget(answerForm.text) }}</div>
|
||||
<div class="d-none">{{ form_widget(answerForm.isRightAnswer) }}</div>
|
||||
<button type="button"
|
||||
class="btn btn-sm {{ answerForm.isRightAnswer.vars.checked ? 'btn-success' : 'btn-danger' }}"
|
||||
title="{{ 'Toggle correct answer'|trans }}"
|
||||
onclick="var cb=this.closest('[data-collection-item]').querySelector('input[type=checkbox]');cb.checked=!cb.checked;this.classList.toggle('btn-success',cb.checked);this.classList.toggle('btn-danger',!cb.checked);this.querySelector('i').className=cb.checked?'bi bi-check-lg':'bi bi-x-lg'">
|
||||
<i class="{{ answerForm.isRightAnswer.vars.checked ? 'bi bi-check-lg' : 'bi bi-x-lg' }}"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-action="bo--form-collection#removeItem"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -40,7 +40,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p class="mb-3">{{ 'Help text for preparing elimination'|trans }}</p>
|
||||
{{ include('backoffice/help/prepare_elimination.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
{% import 'backoffice/partials/answer_row.html.twig' as macros %}
|
||||
|
||||
{% block title %}{{ parent() }}{{ 'Question bank'|trans }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_index') }}">{{ 'Home'|trans }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ season.name }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_question_bank', {seasonCode: season.seasonCode}) }}">{{ 'Question bank'|trans }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ bankQuestion is null ? 'Add question'|trans : 'Edit question'|trans }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<h2 class="mb-3">{{ bankQuestion is null ? 'Add question'|trans : 'Edit question'|trans }}</h2>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.question) }}
|
||||
{{ form_row(form.reusable) }}
|
||||
{{ form_row(form.labels) }}
|
||||
|
||||
<div data-controller="bo--form-collection"
|
||||
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
||||
{{ form_label(form.answers) }}
|
||||
{{ form_errors(form.answers) }}
|
||||
<div data-bo--form-collection-target="collection">
|
||||
{% for answerForm in form.answers %}
|
||||
{{ macros.answer_row(answerForm) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% do form.answers.setRendered %}
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
data-action="bo--form-collection#addItem">{{ 'Add answer'|trans }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-action="bo--form-collection#sortAlphabetically">{{ 'Sort A–Z'|trans }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-action="bo--form-collection#randomize">{{ 'Randomize'|trans }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
{{ include('backoffice/help/quiz_question_bank_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
@@ -0,0 +1,51 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
{% import 'backoffice/partials/answer_row.html.twig' as macros %}
|
||||
|
||||
{% block title %}{{ parent() }}{{ 'Edit question'|trans }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_index') }}">{{ 'Home'|trans }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ season.name }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_quiz_overview', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ quiz.name }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ 'Edit question'|trans }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<h2 class="mb-3">{{ 'Edit question'|trans }}</h2>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.question) }}
|
||||
|
||||
<div data-controller="bo--form-collection"
|
||||
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
||||
{{ form_label(form.answers) }}
|
||||
{{ form_errors(form.answers) }}
|
||||
<div data-bo--form-collection-target="collection">
|
||||
{% for answerForm in form.answers %}
|
||||
{{ macros.answer_row(answerForm) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% do form.answers.setRendered %}
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
data-action="bo--form-collection#addItem">{{ 'Add answer'|trans }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-action="bo--form-collection#sortAlphabetically">{{ 'Sort A–Z'|trans }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-action="bo--form-collection#randomize">{{ 'Randomize'|trans }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
{{ include('backoffice/help/quiz_question_bank_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
@@ -1,3 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-xl-9 col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{% set questions = quiz.questions %}
|
||||
@@ -64,3 +66,8 @@
|
||||
</table>
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save'|trans }}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-xl-3 col-12">
|
||||
{{ include('backoffice/help/quiz_answer_mapping.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-12">
|
||||
<h4 class="mb-3">{{ 'Candidates'|trans }}</h4>
|
||||
<table class="table table-hover mb-3">
|
||||
<thead>
|
||||
@@ -50,3 +52,8 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
{{ include('backoffice/help/quiz_candidates.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,13 +22,24 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div data-controller="bo--quiz">
|
||||
<h4 class="mb-3">{{ 'Quick actions'|trans }}</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-12" data-controller="bo--quiz">
|
||||
<h4 class="mb-3">
|
||||
{{ 'Quick actions'|trans }}
|
||||
{% if quiz.isFinalized %}
|
||||
<span class="badge text-bg-success">{{ 'Finalized'|trans }}</span>
|
||||
{% elseif quiz.isLocked %}
|
||||
<span class="badge text-bg-warning">{{ 'Locked (answers given)'|trans }}</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-secondary">{{ 'Draft'|trans }}</span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<div class="mb-3 btn-group">
|
||||
|
||||
{% if quiz is same as (season.activeQuiz) %}
|
||||
<form action="{{ path('tvdt_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}" method="POST">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('enable_quiz') }}">
|
||||
<input type="hidden" name="redirect_quiz" value="{{ quiz.id }}">
|
||||
<button type="submit" class="btn btn-secondary rounded-0 rounded-start">
|
||||
{{ 'Deactivate Quiz'|trans }}
|
||||
</button>
|
||||
@@ -36,24 +47,44 @@
|
||||
{% else %}
|
||||
<form action="{{ path('tvdt_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}" method="POST">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('enable_quiz') }}">
|
||||
<button type="submit" class="btn btn-primary rounded-0 rounded-start">
|
||||
<button type="submit" class="btn btn-primary rounded-0 rounded-start"
|
||||
{% if not quiz.isFinalized %}disabled data-bs-toggle="tooltip"
|
||||
title="{{ 'The quiz must be finalized before it can be activated'|trans }}"{% endif %}>
|
||||
{{ 'Make active'|trans }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not quiz.isFinalized %}
|
||||
<form action="{{ path('tvdt_backoffice_quiz_finalize', {quiz: quiz.id}) }}" method="POST">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('finalize_quiz') }}">
|
||||
<button type="submit" class="btn btn-success rounded-0"
|
||||
data-bs-toggle="tooltip"
|
||||
title="{{ 'Locks the quiz so it can no longer be edited and makes it ready for candidates to take.'|trans }}">
|
||||
{{ 'Finalize'|trans }}
|
||||
</button>
|
||||
</form>
|
||||
{% elseif not quiz.hasStartedCandidates and quiz is not same as (season.activeQuiz) %}
|
||||
<form action="{{ path('tvdt_backoffice_quiz_unfinalize', {quiz: quiz.id}) }}" method="POST">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('unfinalize_quiz') }}">
|
||||
<button type="submit" class="btn btn-outline-success rounded-0"
|
||||
data-bs-toggle="tooltip"
|
||||
title="{{ 'Re-opens the quiz for editing. Candidates will no longer be able to take the quiz until it is finalized again.'|trans }}">
|
||||
{{ 'Undo finalization'|trans }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<button class="btn btn-danger" data-action="click->bo--quiz#clearQuiz">
|
||||
{{ 'Clear Quiz...'|trans }}
|
||||
</button>
|
||||
<button class="btn btn-danger rounded-0 rounded-end" data-action="click->bo--quiz#deleteQuiz">
|
||||
<button class="btn btn-danger rounded-0 " data-action="click->bo--quiz#deleteQuiz">
|
||||
{{ 'Delete Quiz...'|trans }}
|
||||
</button>
|
||||
<a class="btn btn-secondary rounded-0 rounded-end"
|
||||
href="{{ path('tvdt_backoffice_quiz_export', {quiz: quiz.id}) }}">
|
||||
{{ 'Export to XLSX'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-outline-secondary mb-3"
|
||||
href="{{ path('tvdt_backoffice_quiz_export', {quiz: quiz.id}) }}">
|
||||
{{ 'Export to XLSX'|trans }}
|
||||
</a>
|
||||
|
||||
<h4 class="mb-3">{{ 'Questions'|trans }}</h4>
|
||||
<div class="accordion">
|
||||
{%~ for question in quiz.questions ~%}
|
||||
@@ -73,6 +104,12 @@
|
||||
<div id="question-{{ loop.index0 }}"
|
||||
class="accordion-collapse collapse">
|
||||
<div class="accordion-body">
|
||||
{% if is_granted('QUIZ_MODIFY_CONTENT', question) %}
|
||||
<a class="btn btn-sm btn-outline-secondary mb-2"
|
||||
href="{{ path('tvdt_backoffice_quiz_question_edit', {seasonCode: season.seasonCode, quiz: quiz.id, question: question.id}) }}">
|
||||
<i class="bi bi-pencil"></i> {{ 'Edit'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{%~ for answer in question.answers %}
|
||||
<li{% if answer.isRightAnswer %} class="text-decoration-underline"{% endif %}>
|
||||
@@ -111,3 +148,7 @@
|
||||
csrf_token('delete_quiz'),
|
||||
) }}
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
{{ include('backoffice/help/quiz_overview.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-12">
|
||||
<h4 class="mb-3">{{ 'Score'|trans }}</h4>
|
||||
<div class="btn-toolbar mb-3" role="toolbar">
|
||||
<div class="btn-group me-2">
|
||||
@@ -69,3 +71,8 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
{{ include('backoffice/help/quiz_result.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<p class="mb-3">
|
||||
{{ 'Help text for adding a quiz'|trans }}
|
||||
</p>
|
||||
{{ include('backoffice/help/quiz_add.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_index') }}">{{ 'Home'|trans }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ season.name }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ 'Add blank quiz'|trans }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<h2 class="mb-3">{{ t('Add a quiz to {name}', {name: season.name})|trans }}</h2>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_widget(form.save, {attr: {class: 'btn btn-primary'}}) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
{{ include('backoffice/help/quiz_add_blank.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ parent() }}Backoffice{% endblock %}
|
||||
@@ -11,39 +11,24 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h2 class="mb-3">{{ 'Season'|trans }}: {{ season.name }}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="mb-0 pe-2">{{ 'Quizzes'|trans }}</h4>
|
||||
<a class="link"
|
||||
href="{{ path('tvdt_backoffice_quiz_add', {seasonCode: season.seasonCode}) }}">{{ 'Add'|trans }}</a>
|
||||
</div>
|
||||
<div class="list-group mb-3">
|
||||
{% for quiz in season.quizzes %}
|
||||
<a class="list-group-item list-group-item-action{% if season.activeQuiz == quiz %} active{% endif %}"
|
||||
href="{{ path('tvdt_backoffice_quiz', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ quiz.name }}</a>
|
||||
{% else %}
|
||||
{{ 'No quizzes'|trans }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="mb-0 pe-2">{{ 'Candidates'|trans }}</h4>
|
||||
<a class="link"
|
||||
href="{{ path('tvdt_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
<ul class="mb-3">
|
||||
{% for candidate in season.candidates %}
|
||||
<li>{{ candidate.name }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% set tabs = [
|
||||
{id: 'tests', label: 'Quizzes'|trans, route: 'tvdt_backoffice_season'},
|
||||
{id: 'question-bank', label: 'Question bank'|trans, route: 'tvdt_backoffice_question_bank'},
|
||||
{id: 'candidates', label: 'Candidates'|trans, route: 'tvdt_backoffice_season_candidates'},
|
||||
{id: 'settings', label: 'Settings'|trans, route: 'tvdt_backoffice_season_settings'},
|
||||
] %}
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="mb-0 pe-2">{{ 'Settings'|trans }}</h4>
|
||||
</div>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
<h2 class="mb-3">{{ 'Season'|trans }}: {{ season.name }}</h2>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
{% for tab in tabs %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{ activeTab == tab.id ? ' active' }}" href="{{ path(tab.route, {seasonCode: season.seasonCode}) }}">
|
||||
{{ tab.label }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pt-3">
|
||||
{{ include(template) }}
|
||||
</div>
|
||||
{% endblock body %}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="mb-3">
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="{{ path('tvdt_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}</a>
|
||||
</div>
|
||||
<ul class="mb-3">
|
||||
{% for candidate in season.candidates %}
|
||||
<li>{{ candidate.name }}</li>
|
||||
{% else %}
|
||||
{{ 'No candidates'|trans }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
{{ include('backoffice/help/season_candidates.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,170 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-12">
|
||||
<div class="mb-3">
|
||||
<a class="btn btn-sm btn-outline-primary" href="{{ path('tvdt_backoffice_question_bank_new', {seasonCode: season.seasonCode}) }}">{{ 'Add question'|trans }}</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 mb-3">
|
||||
<a class="badge rounded-pill text-decoration-none text-bg-primary{% if activeLabel is not null %} opacity-50{% endif %}"
|
||||
href="{{ path('tvdt_backoffice_question_bank', {seasonCode: season.seasonCode}) }}">{{ 'All'|trans }}</a>
|
||||
{% for label in season.questionLabels %}
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
<a class="badge rounded-pill text-decoration-none text-bg-{{ label.colour.value }}{% if activeLabel is not same as (label) %} opacity-50{% endif %}"
|
||||
href="{{ path('tvdt_backoffice_question_bank', {seasonCode: season.seasonCode, label: label.slug}) }}">{{ label.name }}</a>
|
||||
<form action="{{ path('tvdt_backoffice_question_bank_label_delete', {seasonCode: season.seasonCode, labelSlug: label.slug}) }}"
|
||||
method="POST" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete_question_label') }}">
|
||||
<button type="submit" class="btn btn-sm btn-link p-0 text-danger" aria-label="{{ 'Remove label'|trans }}"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</span>
|
||||
{% endfor %}
|
||||
|
||||
{% embed 'components/modal.html.twig' with {
|
||||
id: 'addLabelModal',
|
||||
title: 'Add label'|trans,
|
||||
triggerLabel: 'Add label'|trans,
|
||||
} %}
|
||||
{% block modal_body %}
|
||||
<form id="addLabelForm"
|
||||
action="{{ path('tvdt_backoffice_question_bank_labels', {seasonCode: season.seasonCode}) }}"
|
||||
method="POST">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('add_question_label') }}">
|
||||
<div class="mb-3">
|
||||
<label for="addLabelName" class="form-label">{{ 'Name'|trans }}</label>
|
||||
<input type="text" class="form-control" id="addLabelName" name="name" maxlength="64" required autofocus>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label class="form-label d-block">{{ 'Colour'|trans }}</label>
|
||||
<style>
|
||||
.colour-swatch { opacity: .35; cursor: pointer; transition: opacity .1s, box-shadow .1s; font-size: .875rem; }
|
||||
.btn-check:checked + .colour-swatch { opacity: 1; box-shadow: 0 0 0 3px rgba(255,255,255,.6); }
|
||||
.colour-swatch:hover { opacity: .7; }
|
||||
</style>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
{% for colour in labelColours %}
|
||||
<input type="radio" class="btn-check" name="colour"
|
||||
id="addLabelColour_{{ colour.value }}"
|
||||
value="{{ colour.value }}" autocomplete="off"
|
||||
{{ loop.first ? 'checked' }}>
|
||||
<label class="badge rounded-pill text-bg-{{ colour.value }} colour-swatch px-3 py-2"
|
||||
for="addLabelColour_{{ colour.value }}">
|
||||
{{ colour.label|trans }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_footer %}
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
||||
<button type="submit" class="btn btn-primary" form="addLabelForm">{{ 'Add label'|trans }}</button>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</div>
|
||||
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Question'|trans }}</th>
|
||||
<th>{{ 'Labels'|trans }}</th>
|
||||
<th>{{ 'Reusable'|trans }}</th>
|
||||
<th>{{ 'Used in'|trans }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bankQuestion in bankQuestions %}
|
||||
<tr>
|
||||
<td>{{ bankQuestion.question }}</td>
|
||||
<td>
|
||||
{% for label in bankQuestion.labels %}
|
||||
<span class="badge rounded-pill text-bg-{{ label.colour.value }}">{{ label.name }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if bankQuestion.reusable %}
|
||||
<span class="badge text-bg-info">{{ 'Reusable'|trans }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for usage in bankQuestion.usages %}
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span>{{ usage.quiz.name }}</span>
|
||||
<form action="{{ path('tvdt_backoffice_question_bank_unassign', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id, usage: usage.id}) }}"
|
||||
method="POST" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('unassign_bank_question') }}">
|
||||
<button type="submit" class="btn btn-sm btn-link p-0 text-danger" title="{{ 'Unassign'|trans }}">×</button>
|
||||
</form>
|
||||
{% if usage.quiz.isFinalized %}
|
||||
<form action="{{ path('tvdt_backoffice_question_bank_sync', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id, usage: usage.id}) }}"
|
||||
method="POST" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('sync_bank_question') }}">
|
||||
<button type="submit" class="btn btn-sm btn-link p-0 text-primary" title="{{ 'Sync latest changes to this quiz'|trans }}">↻</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
{% if bankQuestion.canBeAssigned and assignableQuizzes|length > 0 %}
|
||||
<form action="{{ path('tvdt_backoffice_question_bank_assign', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id}) }}"
|
||||
method="POST" class="d-inline-flex align-items-center gap-1">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('assign_bank_question') }}">
|
||||
<select class="form-select form-select-sm" name="quiz" required>
|
||||
{% for quiz in assignableQuizzes %}
|
||||
<option value="{{ quiz.id }}">{{ quiz.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary" title="{{ 'Assign'|trans }}"><i class="bi bi-plus-lg"></i></button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="{{ path('tvdt_backoffice_question_bank_edit', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id}) }}"
|
||||
title="{{ 'Edit'|trans }}"><i class="bi bi-pencil"></i></a>
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal"
|
||||
data-bs-target="#deleteBankQuestion-{{ bankQuestion.id }}"
|
||||
title="{{ 'Delete'|trans }}"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="deleteBankQuestion-{{ bankQuestion.id }}" data-bs-backdrop="static"
|
||||
tabindex="-1" aria-labelledby="deleteBankQuestion-{{ bankQuestion.id }}Label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="deleteBankQuestion-{{ bankQuestion.id }}Label">{{ 'Please Confirm'|trans }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start">
|
||||
{{ 'Are you sure you want to delete this question from the question bank?'|trans }}
|
||||
{% if bankQuestion.isUsed %}
|
||||
<br><strong>{{ 'This question has been used in a quiz. The copy in the quiz will not be affected.'|trans }}</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'No'|trans }}</button>
|
||||
<form action="{{ path('tvdt_backoffice_question_bank_delete', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id}) }}"
|
||||
method="POST">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete_bank_question') }}">
|
||||
<button type="submit" class="btn btn-danger">{{ 'Yes'|trans }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">{{ 'No questions in the question bank yet'|trans }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
{{ include('backoffice/help/season_question_bank.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
{{ include('backoffice/help/season_settings.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="{{ path('tvdt_backoffice_quiz_add', {seasonCode: season.seasonCode}) }}">{{ 'Import Quiz from Excel'|trans }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary"
|
||||
href="{{ path('tvdt_backoffice_quiz_add_blank', {seasonCode: season.seasonCode}) }}">{{ 'Add Empty Quiz'|trans }}</a>
|
||||
</div>
|
||||
<div class="list-group mb-3">
|
||||
{% for quiz in season.quizzes %}
|
||||
<a class="list-group-item list-group-item-action{% if season.activeQuiz == quiz %} active{% endif %}"
|
||||
href="{{ path('tvdt_backoffice_quiz', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">
|
||||
{{ quiz.name }}
|
||||
{% if quiz.isFinalized %}
|
||||
<span class="badge text-bg-success">{{ 'Finalized'|trans }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ 'No quizzes'|trans }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
{{ include('backoffice/help/season_tests.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,9 +19,7 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<p class="mb-3">
|
||||
{{ 'Help text for creating a season'|trans }}
|
||||
</p>
|
||||
{{ include('backoffice/help/season_add.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<p class="mb-3">
|
||||
{{ 'Help text for adding candidates'|trans }}
|
||||
</p>
|
||||
{{ include('backoffice/help/season_add_candidates.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{% block modal_trigger %}
|
||||
<button type="button" class="{{ triggerClass|default('btn btn-sm btn-outline-primary') }}"
|
||||
data-bs-toggle="modal" data-bs-target="#{{ id }}">
|
||||
{{ triggerLabel|default('Open') }}
|
||||
</button>
|
||||
{% endblock %}
|
||||
|
||||
<div class="modal fade" id="{{ id }}" tabindex="-1" aria-labelledby="{{ id }}Label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="{{ id }}Label">{{ title }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ 'Close'|trans }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% block modal_body %}{% endblock %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% block modal_footer %}
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user