mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-08 00:20:15 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47077288d5 | |||
| b965b2f10a |
@@ -148,6 +148,11 @@ tests/
|
|||||||
- Coverage excluded from: `src/DataFixtures/`
|
- Coverage excluded from: `src/DataFixtures/`
|
||||||
- Test environment: `APP_ENV=test` (set in phpunit.dist.xml)
|
- Test environment: `APP_ENV=test` (set in phpunit.dist.xml)
|
||||||
|
|
||||||
|
### Testing Conventions (TDD)
|
||||||
|
- **Write the failing test first.** When fixing any PHP-reachable bug, write a PHPUnit test that reproduces the failure before touching the production code. Fix the code until the test passes.
|
||||||
|
- Only skip a test if the bug is purely in JavaScript/frontend where PHPUnit cannot reach it.
|
||||||
|
- Follow the pattern in `tests/Controller/Backoffice/` for controller/integration tests: log in, GET for CSRF token, POST form data, assert redirect, clear entity manager, assert DB state.
|
||||||
|
|
||||||
### Code Style & Standards
|
### Code Style & Standards
|
||||||
- **PHP-CS-Fixer**: Symfony ruleset + risky rules enabled
|
- **PHP-CS-Fixer**: Symfony ruleset + risky rules enabled
|
||||||
- Strict types declaration required
|
- Strict types declaration required
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import 'bootstrap-icons/font/bootstrap-icons.min.css';
|
import 'bootstrap-icons/font/bootstrap-icons.min.css';
|
||||||
import './styles/backoffice.scss';
|
import './styles/backoffice.scss';
|
||||||
import {session as turboSession} from '@hotwired/turbo';
|
import '@hotwired/turbo';
|
||||||
turboSession.drive = false;
|
|
||||||
import './stimulus.js';
|
import './stimulus.js';
|
||||||
import './bootstrap.js';
|
import './bootstrap.js';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|||||||
@@ -6,30 +6,30 @@ export default class extends Controller {
|
|||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.index = this.collectionTarget.children.length;
|
this.index = this.collectionTarget.children.length;
|
||||||
this._setupDrag();
|
|
||||||
this._syncOrdering();
|
this._syncOrdering();
|
||||||
|
|
||||||
if (this.index === 0) {
|
if (this.index === 0) {
|
||||||
this.addItem();
|
this.addItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.collectionTarget.addEventListener('input', (e) => {
|
// `submit` fires on the ancestor <form>, which is outside this controller's
|
||||||
if (e.target.type !== 'text') return;
|
// subtree. Stimulus data-action only works within the controller element, so
|
||||||
const item = e.target.closest('[data-collection-item]');
|
// addEventListener on the form is the only option here.
|
||||||
const last = [...this.collectionTarget.children].at(-1);
|
this._form = this.element.closest('form');
|
||||||
if (item && item === last && e.target.value.trim() !== '') {
|
if (this._form) {
|
||||||
this.addItem();
|
this._submitHandler = () => {
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = this.element.closest('form');
|
|
||||||
if (form) {
|
|
||||||
form.addEventListener('submit', () => {
|
|
||||||
[...this.collectionTarget.children].forEach(item => {
|
[...this.collectionTarget.children].forEach(item => {
|
||||||
const input = item.querySelector('input[type="text"]');
|
const input = item.querySelector('input[type="text"]');
|
||||||
if (input && input.value.trim() === '') item.remove();
|
if (input && input.value.trim() === '') item.remove();
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
this._form.addEventListener('submit', this._submitHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this._form && this._submitHandler) {
|
||||||
|
this._form.removeEventListener('submit', this._submitHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,6 @@ export default class extends Controller {
|
|||||||
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.index);
|
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.index);
|
||||||
const el = item.firstElementChild;
|
const el = item.firstElementChild;
|
||||||
this.collectionTarget.appendChild(el);
|
this.collectionTarget.appendChild(el);
|
||||||
this._makeDraggable(el);
|
|
||||||
this.index++;
|
this.index++;
|
||||||
this._syncOrdering();
|
this._syncOrdering();
|
||||||
}
|
}
|
||||||
@@ -71,59 +70,61 @@ export default class extends Controller {
|
|||||||
this._notifyChange();
|
this._notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
_notifyChange() {
|
autoExpand(event) {
|
||||||
this.element.dispatchEvent(new Event('change', {bubbles: true}));
|
if (event.target.type !== 'text') return;
|
||||||
|
const item = event.target.closest('[data-collection-item]');
|
||||||
|
const last = [...this.collectionTarget.children].at(-1);
|
||||||
|
if (item && item === last && event.target.value.trim() !== '') {
|
||||||
|
this.addItem();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// — drag-and-drop —
|
// — drag-and-drop —
|
||||||
|
|
||||||
_setupDrag() {
|
dragStart(event) {
|
||||||
[...this.collectionTarget.children].forEach(el => this._makeDraggable(el));
|
this._dragging = event.currentTarget.closest('[data-collection-item]');
|
||||||
|
this._dragging.classList.add('opacity-50');
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeDraggable(el) {
|
dragEnd(event) {
|
||||||
const handle = el.querySelector('[data-drag-handle]');
|
event.currentTarget.closest('[data-collection-item]').classList.remove('opacity-50');
|
||||||
if (!handle) return;
|
|
||||||
|
|
||||||
handle.setAttribute('draggable', 'true');
|
|
||||||
|
|
||||||
handle.addEventListener('dragstart', (e) => {
|
|
||||||
this._dragging = el;
|
|
||||||
el.classList.add('opacity-50');
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
});
|
|
||||||
|
|
||||||
handle.addEventListener('dragend', () => {
|
|
||||||
this._dragging = null;
|
this._dragging = null;
|
||||||
el.classList.remove('opacity-50');
|
this.collectionTarget.querySelectorAll('[data-collection-item]').forEach(i =>
|
||||||
this.collectionTarget.querySelectorAll('[data-collection-item]').forEach(i => i.classList.remove('border-top', 'border-bottom', 'border-primary'));
|
i.classList.remove('border-top', 'border-bottom', 'border-primary'),
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
|
||||||
el.addEventListener('dragover', (e) => {
|
dragOver(event) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
const el = event.currentTarget;
|
||||||
if (!this._dragging || this._dragging === el) return;
|
if (!this._dragging || this._dragging === el) return;
|
||||||
e.dataTransfer.dropEffect = 'move';
|
event.dataTransfer.dropEffect = 'move';
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const isBottom = e.clientY > rect.top + rect.height / 2;
|
const isBottom = event.clientY > rect.top + rect.height / 2;
|
||||||
el.classList.toggle('border-top', !isBottom);
|
el.classList.toggle('border-top', !isBottom);
|
||||||
el.classList.toggle('border-bottom', isBottom);
|
el.classList.toggle('border-bottom', isBottom);
|
||||||
el.classList.add('border-primary');
|
el.classList.add('border-primary');
|
||||||
});
|
}
|
||||||
|
|
||||||
el.addEventListener('dragleave', () => {
|
dragLeave(event) {
|
||||||
el.classList.remove('border-top', 'border-bottom', 'border-primary');
|
event.currentTarget.classList.remove('border-top', 'border-bottom', 'border-primary');
|
||||||
});
|
}
|
||||||
|
|
||||||
el.addEventListener('drop', (e) => {
|
drop(event) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
const el = event.currentTarget;
|
||||||
el.classList.remove('border-top', 'border-bottom', 'border-primary');
|
el.classList.remove('border-top', 'border-bottom', 'border-primary');
|
||||||
if (!this._dragging || this._dragging === el) return;
|
if (!this._dragging || this._dragging === el) return;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const isBottom = e.clientY > rect.top + rect.height / 2;
|
const isBottom = event.clientY > rect.top + rect.height / 2;
|
||||||
this.collectionTarget.insertBefore(this._dragging, isBottom ? el.nextSibling : el);
|
this.collectionTarget.insertBefore(this._dragging, isBottom ? el.nextSibling : el);
|
||||||
this._syncOrdering();
|
this._syncOrdering();
|
||||||
this._notifyChange();
|
this._notifyChange();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
_notifyChange() {
|
||||||
|
this.element.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncOrdering() {
|
_syncOrdering() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {Controller} from '@hotwired/stimulus';
|
import {Controller} from '@hotwired/stimulus';
|
||||||
import {Modal} from 'bootstrap';
|
import {Modal} from 'bootstrap';
|
||||||
|
import {visit} from '@hotwired/turbo';
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ['modal', 'frame'];
|
static targets = ['modal', 'frame'];
|
||||||
@@ -11,36 +12,36 @@ export default class extends Controller {
|
|||||||
const titleEl = this.modalTarget.querySelector('.modal-title');
|
const titleEl = this.modalTarget.querySelector('.modal-title');
|
||||||
if (titleEl) titleEl.textContent = modalTitle;
|
if (titleEl) titleEl.textContent = modalTitle;
|
||||||
}
|
}
|
||||||
this._resetDirty();
|
this.resetDirty();
|
||||||
this.frameTarget.innerHTML = '<div class="modal-body text-center py-4"><div class="spinner-border" role="status"></div></div>';
|
this.frameTarget.innerHTML = '<div class="modal-body text-center py-4"><div class="spinner-border" role="status"></div></div>';
|
||||||
this.frameTarget.removeAttribute('src');
|
this.frameTarget.removeAttribute('src');
|
||||||
this.frameTarget.setAttribute('src', src);
|
this.frameTarget.setAttribute('src', src);
|
||||||
Modal.getOrCreateInstance(this.modalTarget).show();
|
Modal.getOrCreateInstance(this.modalTarget).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
frameLoad() {
|
|
||||||
this._bindDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
frameSubmitEnd(event) {
|
frameSubmitEnd(event) {
|
||||||
if (event.detail.success) {
|
if (event.detail.success) {
|
||||||
Modal.getOrCreateInstance(this.modalTarget).hide();
|
Modal.getOrCreateInstance(this.modalTarget).hide();
|
||||||
window.location.reload();
|
visit(window.location.href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_bindDirty() {
|
markDirty() {
|
||||||
|
if (this._dirty) return;
|
||||||
|
this._dirty = true;
|
||||||
|
// Using _config instead of preventDefault on hide.bs.modal because we need
|
||||||
|
// to block only user-triggered dismissal (backdrop click, Escape key) while
|
||||||
|
// keeping programmatic hide() working — frameSubmitEnd() calls hide() after
|
||||||
|
// a successful save and must not be blocked. _config.backdrop/keyboard is
|
||||||
|
// the correct primitive for that distinction and has been stable across all
|
||||||
|
// Bootstrap 5.x releases.
|
||||||
const modal = Modal.getOrCreateInstance(this.modalTarget);
|
const modal = Modal.getOrCreateInstance(this.modalTarget);
|
||||||
const markDirty = () => {
|
|
||||||
modal._config.backdrop = 'static';
|
modal._config.backdrop = 'static';
|
||||||
modal._config.keyboard = false;
|
modal._config.keyboard = false;
|
||||||
};
|
|
||||||
this.frameTarget.addEventListener('input', markDirty, {once: true});
|
|
||||||
this.frameTarget.addEventListener('change', markDirty, {once: true});
|
|
||||||
this.modalTarget.addEventListener('hidden.bs.modal', () => this._resetDirty(), {once: true});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetDirty() {
|
resetDirty() {
|
||||||
|
this._dirty = false;
|
||||||
const modal = Modal.getOrCreateInstance(this.modalTarget);
|
const modal = Modal.getOrCreateInstance(this.modalTarget);
|
||||||
modal._config.backdrop = true;
|
modal._config.backdrop = true;
|
||||||
modal._config.keyboard = true;
|
modal._config.keyboard = true;
|
||||||
|
|||||||
@@ -1,58 +1,43 @@
|
|||||||
import {Controller} from '@hotwired/stimulus';
|
import {Controller} from '@hotwired/stimulus';
|
||||||
import {Modal} from 'bootstrap';
|
|
||||||
|
|
||||||
const RETRY_DELAY_MS = 1000;
|
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ['list', 'item', 'status', 'noticeModal'];
|
static targets = ['list', 'item', 'status'];
|
||||||
static values = {
|
static values = {
|
||||||
reorderUrl: String,
|
reorderUrl: String,
|
||||||
csrf: String,
|
csrf: String,
|
||||||
canModify: Boolean,
|
|
||||||
savedLabel: String,
|
savedLabel: String,
|
||||||
errorLabel: String,
|
errorLabel: String,
|
||||||
|
errorHint: String,
|
||||||
};
|
};
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
if (this.canModifyValue) {
|
this._locked = false;
|
||||||
this._setupDrag();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupDrag() {
|
dragStart(event) {
|
||||||
this.itemTargets.forEach(el => {
|
const item = event.currentTarget.closest('[data-bo--question-list-target="item"]');
|
||||||
const handle = el.querySelector('[data-drag-handle]');
|
this._dragging = item;
|
||||||
if (!handle) return;
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
setTimeout(() => item.classList.add('opacity-50'), 0);
|
||||||
handle.setAttribute('draggable', 'true');
|
|
||||||
|
|
||||||
handle.addEventListener('dragstart', (e) => {
|
|
||||||
if (this._locked) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this._dragging = el;
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
setTimeout(() => el.classList.add('opacity-50'), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
handle.addEventListener('dragend', () => {
|
dragEnd(event) {
|
||||||
el.classList.remove('opacity-50');
|
const item = event.currentTarget.closest('[data-bo--question-list-target="item"]');
|
||||||
|
item.classList.remove('opacity-50');
|
||||||
this._dragging = null;
|
this._dragging = null;
|
||||||
this._removePlaceholder();
|
this._removePlaceholder();
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this.listTarget.addEventListener('dragover', (e) => {
|
dragOver(event) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
if (!this._dragging) return;
|
if (!this._dragging) return;
|
||||||
e.dataTransfer.dropEffect = 'move';
|
event.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
const target = e.target.closest('[data-bo--question-list-target="item"]');
|
const target = event.target.closest('[data-bo--question-list-target="item"]');
|
||||||
if (!target || target === this._dragging) return;
|
if (!target || target === this._dragging) return;
|
||||||
|
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
const insertBefore = e.clientY > rect.top + rect.height / 2 ? target.nextSibling : target;
|
const insertBefore = event.clientY > rect.top + rect.height / 2 ? target.nextSibling : target;
|
||||||
|
|
||||||
if (!this._placeholder) {
|
if (!this._placeholder) {
|
||||||
this._placeholder = document.createElement('div');
|
this._placeholder = document.createElement('div');
|
||||||
@@ -63,21 +48,20 @@ export default class extends Controller {
|
|||||||
if (this._placeholder.nextSibling !== insertBefore) {
|
if (this._placeholder.nextSibling !== insertBefore) {
|
||||||
this.listTarget.insertBefore(this._placeholder, insertBefore);
|
this.listTarget.insertBefore(this._placeholder, insertBefore);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
this.listTarget.addEventListener('dragleave', (e) => {
|
dragLeave(event) {
|
||||||
if (!e.relatedTarget || !this.listTarget.contains(e.relatedTarget)) {
|
if (!event.relatedTarget || !this.listTarget.contains(event.relatedTarget)) {
|
||||||
this._removePlaceholder();
|
this._removePlaceholder();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
this.listTarget.addEventListener('drop', async (e) => {
|
async drop(event) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
if (!this._dragging || !this._placeholder) return;
|
if (!this._dragging || !this._placeholder || this._locked) return;
|
||||||
this.listTarget.insertBefore(this._dragging, this._placeholder);
|
this.listTarget.insertBefore(this._dragging, this._placeholder);
|
||||||
this._removePlaceholder();
|
this._removePlaceholder();
|
||||||
await this._persistOrder();
|
await this._persistOrder();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_removePlaceholder() {
|
_removePlaceholder() {
|
||||||
@@ -114,40 +98,26 @@ export default class extends Controller {
|
|||||||
if (numberEl) numberEl.textContent = String(i + 1);
|
if (numberEl) numberEl.textContent = String(i + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const attempt = async () => {
|
for (let attempt = 0; attempt < 2; attempt++) {
|
||||||
|
try {
|
||||||
const res = await fetch(this.reorderUrlValue, {method: 'POST', body: params});
|
const res = await fetch(this.reorderUrlValue, {method: 'POST', body: params});
|
||||||
if (!res.ok) {
|
if (res.ok) {
|
||||||
throw new Error(`Unexpected response status: ${res.status}`);
|
this._setStatus('saved');
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await attempt();
|
|
||||||
} catch {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
|
||||||
try {
|
|
||||||
await attempt();
|
|
||||||
} catch {
|
|
||||||
this._setStatus('error');
|
|
||||||
this._lockReordering();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// network error — retry on first attempt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setStatus('saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
_lockReordering() {
|
|
||||||
this._locked = true;
|
this._locked = true;
|
||||||
this.itemTargets.forEach(el => {
|
this._setStatus('error');
|
||||||
const handle = el.querySelector('[data-drag-handle]');
|
|
||||||
if (handle) {
|
const alert = document.createElement('div');
|
||||||
handle.removeAttribute('draggable');
|
alert.className = 'alert alert-danger alert-dismissible mt-3';
|
||||||
handle.classList.add('opacity-25', 'pe-none');
|
alert.setAttribute('role', 'alert');
|
||||||
}
|
const hint = this.errorHintValue || 'Refresh the page to try again.';
|
||||||
});
|
alert.innerHTML = `${this.errorLabelValue || 'Error saving order'} — ${hint} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
|
||||||
if (this.hasNoticeModalTarget) {
|
this.listTarget.after(alert);
|
||||||
Modal.getOrCreateInstance(this.noticeModalTarget).show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+37
-38
@@ -9525,20 +9525,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nikic/php-parser",
|
"name": "nikic/php-parser",
|
||||||
"version": "v5.7.0",
|
"version": "v5.8.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||||
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
|
"reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
|
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
|
||||||
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
|
"reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-ctype": "*",
|
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-tokenizer": "*",
|
"ext-tokenizer": "*",
|
||||||
"php": ">=7.4"
|
"php": ">=7.4"
|
||||||
@@ -9577,9 +9576,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
|
"source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0"
|
||||||
},
|
},
|
||||||
"time": "2025-12-06T11:56:16+00:00"
|
"time": "2026-07-04T14:30:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phar-io/manifest",
|
"name": "phar-io/manifest",
|
||||||
@@ -9749,11 +9748,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/f0fe3fb03bb53ce68cc2416785b260e62226ec27",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||||
"reference": "f0fe3fb03bb53ce68cc2416785b260e62226ec27",
|
"reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -9809,7 +9808,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-07-03T07:00:23+00:00"
|
"time": "2026-07-05T06:31:06+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan-doctrine",
|
"name": "phpstan/phpstan-doctrine",
|
||||||
@@ -9890,16 +9889,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan-phpunit",
|
"name": "phpstan/phpstan-phpunit",
|
||||||
"version": "2.0.17",
|
"version": "2.0.18",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpstan/phpstan-phpunit.git",
|
"url": "https://github.com/phpstan/phpstan-phpunit.git",
|
||||||
"reference": "c2f977551f0736d60467b3d754b2e0cf4e337b3f"
|
"reference": "f5dc20ff8082d02339b60cab68ec3eb0d859fb30"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/c2f977551f0736d60467b3d754b2e0cf4e337b3f",
|
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f5dc20ff8082d02339b60cab68ec3eb0d859fb30",
|
||||||
"reference": "c2f977551f0736d60467b3d754b2e0cf4e337b3f",
|
"reference": "f5dc20ff8082d02339b60cab68ec3eb0d859fb30",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -9942,9 +9941,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
|
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
|
||||||
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.17"
|
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.18"
|
||||||
},
|
},
|
||||||
"time": "2026-06-29T05:32:23+00:00"
|
"time": "2026-07-04T12:16:09+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan-symfony",
|
"name": "phpstan/phpstan-symfony",
|
||||||
@@ -10022,16 +10021,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
"version": "14.2.2",
|
"version": "14.2.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||||
"reference": "10d7da3628a99289cdf4c662dd7f0d73f1baec83"
|
"reference": "82f6e49ff224e2cde923d74425e583a883910783"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/10d7da3628a99289cdf4c662dd7f0d73f1baec83",
|
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/82f6e49ff224e2cde923d74425e583a883910783",
|
||||||
"reference": "10d7da3628a99289cdf4c662dd7f0d73f1baec83",
|
"reference": "82f6e49ff224e2cde923d74425e583a883910783",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -10039,7 +10038,7 @@
|
|||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-xmlwriter": "*",
|
"ext-xmlwriter": "*",
|
||||||
"nikic/php-parser": "^5.7.0",
|
"nikic/php-parser": "^5.8.0",
|
||||||
"php": ">=8.4",
|
"php": ">=8.4",
|
||||||
"phpunit/php-text-template": "^6.0",
|
"phpunit/php-text-template": "^6.0",
|
||||||
"sebastian/complexity": "^6.0",
|
"sebastian/complexity": "^6.0",
|
||||||
@@ -10050,7 +10049,7 @@
|
|||||||
"theseer/tokenizer": "^2.0.1"
|
"theseer/tokenizer": "^2.0.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^13.2.0"
|
"phpunit/phpunit": "^13.2.2"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-pcov": "PHP extension that provides line coverage",
|
"ext-pcov": "PHP extension that provides line coverage",
|
||||||
@@ -10088,7 +10087,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.2"
|
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.3"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -10108,7 +10107,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-06-08T11:50:38+00:00"
|
"time": "2026-07-06T15:04:02+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-file-iterator",
|
"name": "phpunit/php-file-iterator",
|
||||||
@@ -10405,24 +10404,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "13.2.2",
|
"version": "13.2.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "492c067e618de7b3c76105082c90f9d2833401b7"
|
"reference": "d76d0e24225e587d6a5f0c6f6d9fef0d90712b54"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/492c067e618de7b3c76105082c90f9d2833401b7",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d76d0e24225e587d6a5f0c6f6d9fef0d90712b54",
|
||||||
"reference": "492c067e618de7b3c76105082c90f9d2833401b7",
|
"reference": "d76d0e24225e587d6a5f0c6f6d9fef0d90712b54",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-xml": "*",
|
|
||||||
"ext-xmlwriter": "*",
|
"ext-xmlwriter": "*",
|
||||||
"myclabs/deep-copy": "^1.13.4",
|
"myclabs/deep-copy": "^1.13.4",
|
||||||
"phar-io/manifest": "^2.0.4",
|
"phar-io/manifest": "^2.0.4",
|
||||||
@@ -10485,7 +10484,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.2"
|
"source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.3"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -10493,7 +10492,7 @@
|
|||||||
"type": "other"
|
"type": "other"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-06-29T13:36:29+00:00"
|
"time": "2026-07-06T14:55:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "react/cache",
|
"name": "react/cache",
|
||||||
@@ -11023,16 +11022,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "rector/rector",
|
"name": "rector/rector",
|
||||||
"version": "2.5.2",
|
"version": "2.5.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/rectorphp/rector.git",
|
"url": "https://github.com/rectorphp/rector.git",
|
||||||
"reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24"
|
"reference": "adaa18d7cd6b3c960967cfbc98c03efb3116ac0e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/49ff6339174bdbdf50b0b35ecbcff14a05ac9e24",
|
"url": "https://api.github.com/repos/rectorphp/rector/zipball/adaa18d7cd6b3c960967cfbc98c03efb3116ac0e",
|
||||||
"reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24",
|
"reference": "adaa18d7cd6b3c960967cfbc98c03efb3116ac0e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -11071,7 +11070,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/rectorphp/rector/issues",
|
"issues": "https://github.com/rectorphp/rector/issues",
|
||||||
"source": "https://github.com/rectorphp/rector/tree/2.5.2"
|
"source": "https://github.com/rectorphp/rector/tree/2.5.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -11079,7 +11078,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-06-22T11:39:33+00:00"
|
"time": "2026-07-06T12:41:46+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
|
|||||||
@@ -90,11 +90,13 @@ class QuestionBankController extends AbstractController
|
|||||||
|
|
||||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||||
|
|
||||||
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, ['season' => $season]);
|
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, [
|
||||||
|
'season' => $season,
|
||||||
|
'action' => $this->generateUrl('tvdt_backoffice_question_bank_new', ['seasonCode' => $season->seasonCode]),
|
||||||
|
]);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$this->applyAnswerOrdering($bankQuestion);
|
|
||||||
$season->addBankQuestion($bankQuestion);
|
$season->addBankQuestion($bankQuestion);
|
||||||
$this->em->persist($bankQuestion);
|
$this->em->persist($bankQuestion);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
@@ -138,7 +140,13 @@ class QuestionBankController extends AbstractController
|
|||||||
|
|
||||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||||
|
|
||||||
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, ['season' => $season]);
|
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, [
|
||||||
|
'season' => $season,
|
||||||
|
'action' => $this->generateUrl('tvdt_backoffice_question_bank_edit', [
|
||||||
|
'seasonCode' => $season->seasonCode,
|
||||||
|
'bankQuestion' => $bankQuestion->id,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
@@ -381,14 +389,6 @@ class QuestionBankController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyAnswerOrdering(BankQuestion $bankQuestion): void
|
|
||||||
{
|
|
||||||
$ordering = 1;
|
|
||||||
foreach ($bankQuestion->answers as $answer) {
|
|
||||||
$answer->ordering = $ordering++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function syncUsagesAfterEdit(BankQuestion $bankQuestion): void
|
private function syncUsagesAfterEdit(BankQuestion $bankQuestion): void
|
||||||
{
|
{
|
||||||
$pendingNames = [];
|
$pendingNames = [];
|
||||||
|
|||||||
@@ -46,11 +46,16 @@ class QuizQuestionController extends AbstractController
|
|||||||
|
|
||||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||||
|
|
||||||
$form = $this->createForm(QuestionFormType::class, $question);
|
$form = $this->createForm(QuestionFormType::class, $question, [
|
||||||
|
'action' => $this->generateUrl('tvdt_backoffice_quiz_question_edit', [
|
||||||
|
'seasonCode' => $season->seasonCode,
|
||||||
|
'quiz' => $quiz->id,
|
||||||
|
'question' => $question->id,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$this->applyAnswerOrdering($question);
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
$this->addFlash(FlashType::Success, $this->translator->trans('Question updated'));
|
$this->addFlash(FlashType::Success, $this->translator->trans('Question updated'));
|
||||||
@@ -83,6 +88,7 @@ class QuizQuestionController extends AbstractController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
#[Route(
|
#[Route(
|
||||||
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/question/{question}/view',
|
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/question/{question}/view',
|
||||||
name: 'tvdt_backoffice_quiz_question_view',
|
name: 'tvdt_backoffice_quiz_question_view',
|
||||||
@@ -127,6 +133,10 @@ class QuizQuestionController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\count(array_unique($ordering)) !== \count($questionsById)) {
|
||||||
|
throw new BadRequestHttpException('Ordering must include every question exactly once');
|
||||||
|
}
|
||||||
|
|
||||||
$position = 1;
|
$position = 1;
|
||||||
foreach ($ordering as $questionId) {
|
foreach ($ordering as $questionId) {
|
||||||
$questionsById[$questionId]->ordering = $position++;
|
$questionsById[$questionId]->ordering = $position++;
|
||||||
@@ -136,12 +146,4 @@ class QuizQuestionController extends AbstractController
|
|||||||
|
|
||||||
return new Response('', Response::HTTP_NO_CONTENT);
|
return new Response('', Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyAnswerOrdering(Question $question): void
|
|
||||||
{
|
|
||||||
$ordering = 1;
|
|
||||||
foreach ($question->answers as $answer) {
|
|
||||||
$answer->ordering = $ordering++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ class Question implements \Stringable
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function removeAnswer(Answer $answer): static
|
||||||
|
{
|
||||||
|
$this->answers->removeElement($answer);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return $this->question ?? '';
|
return $this->question ?? '';
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class BankQuestionFormType extends AbstractType
|
|||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'choice_attr' => static fn (QuestionLabel $label): array => ['data-colour' => $label->colour->value],
|
||||||
'query_builder' => static fn (QuestionLabelRepository $repository): QueryBuilder => $repository
|
'query_builder' => static fn (QuestionLabelRepository $repository): QueryBuilder => $repository
|
||||||
->createQueryBuilder('l')
|
->createQueryBuilder('l')
|
||||||
->where('l.season = :season')
|
->where('l.season = :season')
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{% macro answer_row(answerForm) %}
|
{% macro answer_row(answerForm) %}
|
||||||
<div class="d-flex align-items-center gap-2 mb-2" data-collection-item>
|
<div class="d-flex align-items-center gap-2 mb-2" data-collection-item
|
||||||
|
data-action="dragover->bo--form-collection#dragOver dragleave->bo--form-collection#dragLeave drop->bo--form-collection#drop">
|
||||||
{{ form_widget(answerForm.ordering) }}
|
{{ 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>
|
<span class="text-muted" data-drag-handle style="cursor: grab" title="{{ 'Drag to reorder'|trans }}"
|
||||||
|
draggable="true"
|
||||||
|
data-action="dragstart->bo--form-collection#dragStart dragend->bo--form-collection#dragEnd"><i class="bi bi-grip-vertical"></i></span>
|
||||||
<div class="flex-grow-1">{{ form_widget(answerForm.text) }}</div>
|
<div class="flex-grow-1">{{ form_widget(answerForm.text) }}</div>
|
||||||
<div class="d-none">{{ form_widget(answerForm.isRightAnswer) }}</div>
|
<div class="d-none">{{ form_widget(answerForm.isRightAnswer) }}</div>
|
||||||
<button type="button" tabindex="-1"
|
<button type="button"
|
||||||
class="btn btn-sm {{ answerForm.isRightAnswer.vars.checked ? 'btn-success' : 'btn-danger' }}"
|
class="btn btn-sm {{ answerForm.isRightAnswer.vars.checked ? 'btn-success' : 'btn-danger' }}"
|
||||||
title="{{ 'Toggle correct answer'|trans }}"
|
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'">
|
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'">
|
||||||
|
|||||||
@@ -3,13 +3,30 @@
|
|||||||
{{ form_start(form, {attr: {novalidate: 'novalidate'}}) }}
|
{{ form_start(form, {attr: {novalidate: 'novalidate'}}) }}
|
||||||
{{ form_row(form.question) }}
|
{{ form_row(form.question) }}
|
||||||
{{ form_row(form.reusable) }}
|
{{ form_row(form.reusable) }}
|
||||||
{{ form_row(form.labels) }}
|
<div class="mb-3">
|
||||||
|
{{ form_label(form.labels) }}
|
||||||
|
{{ form_errors(form.labels) }}
|
||||||
|
{% for labelChoice in form.labels %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input"
|
||||||
|
id="{{ labelChoice.vars.id }}"
|
||||||
|
name="{{ labelChoice.vars.full_name }}"
|
||||||
|
value="{{ labelChoice.vars.value }}"
|
||||||
|
{% if labelChoice.vars.checked %}checked="checked"{% endif %}>
|
||||||
|
<label class="form-check-label" for="{{ labelChoice.vars.id }}">
|
||||||
|
<span class="badge rounded-pill text-bg-{{ labelChoice.vars.attr['data-colour'] }}">{{ labelChoice.vars.label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% do form.labels.setRendered %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div data-controller="bo--form-collection"
|
<div data-controller="bo--form-collection"
|
||||||
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
||||||
{{ form_label(form.answers) }}
|
{{ form_label(form.answers) }}
|
||||||
{{ form_errors(form.answers) }}
|
{{ form_errors(form.answers) }}
|
||||||
<div data-bo--form-collection-target="collection">
|
<div data-bo--form-collection-target="collection"
|
||||||
|
data-action="input->bo--form-collection#autoExpand">
|
||||||
{% for answerForm in form.answers %}
|
{% for answerForm in form.answers %}
|
||||||
{{ macros.answer_row(answerForm) }}
|
{{ macros.answer_row(answerForm) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -5,12 +5,29 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{{ form_row(form.question) }}
|
{{ form_row(form.question) }}
|
||||||
{{ form_row(form.reusable) }}
|
{{ form_row(form.reusable) }}
|
||||||
{{ form_row(form.labels) }}
|
<div class="mb-3">
|
||||||
|
{{ form_label(form.labels) }}
|
||||||
|
{{ form_errors(form.labels) }}
|
||||||
|
{% for labelChoice in form.labels %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input"
|
||||||
|
id="{{ labelChoice.vars.id }}"
|
||||||
|
name="{{ labelChoice.vars.full_name }}"
|
||||||
|
value="{{ labelChoice.vars.value }}"
|
||||||
|
{% if labelChoice.vars.checked %}checked="checked"{% endif %}>
|
||||||
|
<label class="form-check-label" for="{{ labelChoice.vars.id }}">
|
||||||
|
<span class="badge rounded-pill text-bg-{{ labelChoice.vars.attr['data-colour'] }}">{{ labelChoice.vars.label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% do form.labels.setRendered %}
|
||||||
|
</div>
|
||||||
<div data-controller="bo--form-collection"
|
<div data-controller="bo--form-collection"
|
||||||
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
||||||
{{ form_label(form.answers) }}
|
{{ form_label(form.answers) }}
|
||||||
{{ form_errors(form.answers) }}
|
{{ form_errors(form.answers) }}
|
||||||
<div data-bo--form-collection-target="collection">
|
<div data-bo--form-collection-target="collection"
|
||||||
|
data-action="input->bo--form-collection#autoExpand">
|
||||||
{% for answerForm in form.answers %}
|
{% for answerForm in form.answers %}
|
||||||
{{ macros.answer_row(answerForm) }}
|
{{ macros.answer_row(answerForm) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
||||||
{{ form_label(form.answers) }}
|
{{ form_label(form.answers) }}
|
||||||
{{ form_errors(form.answers) }}
|
{{ form_errors(form.answers) }}
|
||||||
<div data-bo--form-collection-target="collection">
|
<div data-bo--form-collection-target="collection"
|
||||||
|
data-action="input->bo--form-collection#autoExpand">
|
||||||
{% for answerForm in form.answers %}
|
{% for answerForm in form.answers %}
|
||||||
{{ macros.answer_row(answerForm) }}
|
{{ macros.answer_row(answerForm) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
data-bo--form-collection-prototype-value="{{ macros.answer_row(form.answers.vars.prototype)|e('html_attr') }}">
|
||||||
{{ form_label(form.answers) }}
|
{{ form_label(form.answers) }}
|
||||||
{{ form_errors(form.answers) }}
|
{{ form_errors(form.answers) }}
|
||||||
<div data-bo--form-collection-target="collection">
|
<div data-bo--form-collection-target="collection"
|
||||||
|
data-action="input->bo--form-collection#autoExpand">
|
||||||
{% for answerForm in form.answers %}
|
{% for answerForm in form.answers %}
|
||||||
{{ macros.answer_row(answerForm) }}
|
{{ macros.answer_row(answerForm) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -86,19 +86,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-controller="bo--question-list bo--modal"
|
<div data-controller="bo--question-list bo--modal"
|
||||||
data-action="turbo:frame-load->bo--modal#frameLoad turbo:submit-end->bo--modal#frameSubmitEnd"
|
data-action="turbo:submit-end->bo--modal#frameSubmitEnd"
|
||||||
data-bo--question-list-reorder-url-value="{{ path('tvdt_backoffice_quiz_questions_reorder', {seasonCode: season.seasonCode, quiz: quiz.id}) }}"
|
data-bo--question-list-reorder-url-value="{{ path('tvdt_backoffice_quiz_questions_reorder', {seasonCode: season.seasonCode, quiz: quiz.id}) }}"
|
||||||
data-bo--question-list-csrf-value="{{ csrf_token('question_reorder') }}"
|
data-bo--question-list-csrf-value="{{ csrf_token('question_reorder') }}"
|
||||||
data-bo--question-list-can-modify-value="{{ is_granted('QUIZ_MODIFY_CONTENT', quiz) ? 'true' : 'false' }}"
|
|
||||||
data-bo--question-list-saved-label-value="{{ 'Order saved'|trans }}"
|
data-bo--question-list-saved-label-value="{{ 'Order saved'|trans }}"
|
||||||
data-bo--question-list-error-label-value="{{ 'Error saving order'|trans }}">
|
data-bo--question-list-error-label-value="{{ 'Error saving order'|trans }}"
|
||||||
|
data-bo--question-list-error-hint-value="{{ 'Refresh the page to try again.'|trans }}">
|
||||||
|
|
||||||
<h4 class="mb-3 d-flex align-items-center gap-2">
|
<h4 class="mb-3 d-flex align-items-center gap-2">
|
||||||
{{ 'Questions'|trans }}
|
{{ 'Questions'|trans }}
|
||||||
<span class="badge d-none fw-normal" style="font-size:.7rem;vertical-align:baseline" data-bo--question-list-target="status"></span>
|
<span class="badge d-none fw-normal" style="font-size:.7rem;vertical-align:baseline" data-bo--question-list-target="status"></span>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div data-bo--question-list-target="list">
|
<div data-bo--question-list-target="list"
|
||||||
|
{% if is_granted('QUIZ_MODIFY_CONTENT', quiz) %}data-action="dragover->bo--question-list#dragOver dragleave->bo--question-list#dragLeave drop->bo--question-list#drop"{% endif %}>
|
||||||
{%~ for question in quiz.questions ~%}
|
{%~ for question in quiz.questions ~%}
|
||||||
<div class="card mb-2"
|
<div class="card mb-2"
|
||||||
data-bo--question-list-target="item"
|
data-bo--question-list-target="item"
|
||||||
@@ -106,7 +107,9 @@
|
|||||||
<div class="card-body py-2">
|
<div class="card-body py-2">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
{% if is_granted('QUIZ_MODIFY_CONTENT', question) %}
|
{% if is_granted('QUIZ_MODIFY_CONTENT', question) %}
|
||||||
<span class="text-muted" style="cursor:grab" data-drag-handle>
|
<span class="text-muted" style="cursor:grab"
|
||||||
|
draggable="true"
|
||||||
|
data-action="dragstart->bo--question-list#dragStart dragend->bo--question-list#dragEnd">
|
||||||
<i class="bi bi-grip-vertical"></i>
|
<i class="bi bi-grip-vertical"></i>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -135,12 +138,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ 'EMPTY'|trans }}
|
{{ 'No questions have been added to this quiz yet.'|trans }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" tabindex="-1"
|
<div class="modal fade" tabindex="-1"
|
||||||
data-bo--modal-target="modal"
|
data-bo--modal-target="modal"
|
||||||
|
data-action="hidden.bs.modal->bo--modal#resetDirty"
|
||||||
aria-labelledby="questionEditModalLabel" aria-hidden="true">
|
aria-labelledby="questionEditModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -148,26 +152,9 @@
|
|||||||
<h5 class="modal-title" id="questionEditModalLabel">{{ 'Edit question'|trans }}</h5>
|
<h5 class="modal-title" id="questionEditModalLabel">{{ 'Edit question'|trans }}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<turbo-frame id="question-modal-frame" data-bo--modal-target="frame"></turbo-frame>
|
<turbo-frame id="question-modal-frame"
|
||||||
</div>
|
data-bo--modal-target="frame"
|
||||||
</div>
|
data-action="input->bo--modal#markDirty change->bo--modal#markDirty"></turbo-frame>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal fade" tabindex="-1"
|
|
||||||
data-bo--question-list-target="noticeModal"
|
|
||||||
aria-labelledby="questionReorderErrorModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="questionReorderErrorModalLabel">{{ 'Could not save order'|trans }}</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
{{ 'The new question order could not be saved. Reordering has been disabled until you refresh the page.'|trans }}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Close'|trans }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 col-12" data-controller="bo--modal"
|
<div class="col-md-8 col-12" data-controller="bo--modal"
|
||||||
data-action="turbo:frame-load->bo--modal#frameLoad turbo:submit-end->bo--modal#frameSubmitEnd">
|
data-action="turbo:submit-end->bo--modal#frameSubmitEnd">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button class="btn btn-sm btn-outline-primary"
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
data-action="click->bo--modal#open"
|
data-action="click->bo--modal#open"
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
data-action="click->bo--modal#open"
|
data-action="click->bo--modal#open"
|
||||||
data-src="{{ path('tvdt_backoffice_question_bank_edit', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id}) }}"
|
data-src="{{ path('tvdt_backoffice_question_bank_edit', {seasonCode: season.seasonCode, bankQuestion: bankQuestion.id}) }}"
|
||||||
|
data-modal-title="{{ 'Edit question'|trans }}"
|
||||||
title="{{ 'Edit'|trans }}"><i class="bi bi-pencil"></i></button>
|
title="{{ 'Edit'|trans }}"><i class="bi bi-pencil"></i></button>
|
||||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal"
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal"
|
||||||
data-bs-target="#deleteBankQuestion-{{ bankQuestion.id }}"
|
data-bs-target="#deleteBankQuestion-{{ bankQuestion.id }}"
|
||||||
@@ -172,6 +173,7 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="modal fade" tabindex="-1"
|
<div class="modal fade" tabindex="-1"
|
||||||
data-bo--modal-target="modal"
|
data-bo--modal-target="modal"
|
||||||
|
data-action="hidden.bs.modal->bo--modal#resetDirty"
|
||||||
aria-labelledby="bankQuestionEditModalLabel" aria-hidden="true">
|
aria-labelledby="bankQuestionEditModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -179,7 +181,9 @@
|
|||||||
<h5 class="modal-title" id="bankQuestionEditModalLabel">{{ 'Edit question'|trans }}</h5>
|
<h5 class="modal-title" id="bankQuestionEditModalLabel">{{ 'Edit question'|trans }}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<turbo-frame id="bank-question-modal-frame" data-bo--modal-target="frame"></turbo-frame>
|
<turbo-frame id="bank-question-modal-frame"
|
||||||
|
data-bo--modal-target="frame"
|
||||||
|
data-action="input->bo--modal#markDirty change->bo--modal#markDirty"></turbo-frame>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
|||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Tvdt\Controller\Backoffice\QuestionBankController;
|
use Tvdt\Controller\Backoffice\QuestionBankController;
|
||||||
|
use Tvdt\Entity\BankAnswer;
|
||||||
use Tvdt\Entity\BankQuestion;
|
use Tvdt\Entity\BankQuestion;
|
||||||
use Tvdt\Entity\Question;
|
use Tvdt\Entity\Question;
|
||||||
use Tvdt\Entity\QuestionLabel;
|
use Tvdt\Entity\QuestionLabel;
|
||||||
@@ -298,6 +299,79 @@ final class QuestionBankControllerTest extends WebTestCase
|
|||||||
$this->assertResponseStatusCodeSame(403);
|
$this->assertResponseStatusCodeSame(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateBankQuestionPreservesAnswerOrdering(): void
|
||||||
|
{
|
||||||
|
$this->loginAsOwner();
|
||||||
|
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank/new');
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$token = (string) $crawler->filter('input[name="bank_question_form[_token]"]')->attr('value');
|
||||||
|
|
||||||
|
// Submit 3 answers with non-sequential ordering values.
|
||||||
|
// The stored ordering field (not the submission index) must dictate retrieval order.
|
||||||
|
$this->client->request(Request::METHOD_POST, '/backoffice/season/krtek/question-bank/new', [
|
||||||
|
'bank_question_form' => [
|
||||||
|
'question' => 'Volgorderingstest nieuwe vraag',
|
||||||
|
'answers' => [
|
||||||
|
0 => ['text' => 'Antwoord C', 'isRightAnswer' => '1', 'ordering' => '5'],
|
||||||
|
1 => ['text' => 'Antwoord A', 'ordering' => '1'],
|
||||||
|
2 => ['text' => 'Antwoord B', 'ordering' => '3'],
|
||||||
|
],
|
||||||
|
'_token' => $token,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$bankQuestion = $this->getBankQuestion('Volgorderingstest nieuwe vraag');
|
||||||
|
$answers = $bankQuestion->answers->toArray();
|
||||||
|
$this->assertCount(3, $answers);
|
||||||
|
// @OrderBy(['ordering' => 'ASC']): ordering 1 → 3 → 5
|
||||||
|
$this->assertSame('Antwoord A', $answers[0]->text);
|
||||||
|
$this->assertSame('Antwoord B', $answers[1]->text);
|
||||||
|
$this->assertSame('Antwoord C', $answers[2]->text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditBankQuestionPreservesAnswerOrdering(): void
|
||||||
|
{
|
||||||
|
$this->loginAsOwner();
|
||||||
|
$bankQuestion = $this->getBankQuestion('Wat at de Krtek als ontbijt?');
|
||||||
|
// Fixture answers in insertion order (all have ordering=0): Brood (correct), Yoghurt, Niks
|
||||||
|
|
||||||
|
$url = \sprintf('/backoffice/season/krtek/question-bank/%s/edit', $bankQuestion->id);
|
||||||
|
$crawler = $this->client->request(Request::METHOD_GET, $url);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$token = (string) $crawler->filter('input[name="bank_question_form[_token]"]')->attr('value');
|
||||||
|
|
||||||
|
$answers = $bankQuestion->answers->toArray();
|
||||||
|
$this->assertCount(3, $answers);
|
||||||
|
$texts = array_map(static fn (BankAnswer $a): string => $a->text, $answers);
|
||||||
|
|
||||||
|
// Assign ordering values: first answer gets 4, second gets 0, third gets 2.
|
||||||
|
// Expected retrieval order after @OrderBy ASC: index 1 (0) → index 2 (2) → index 0 (4).
|
||||||
|
$this->client->request(Request::METHOD_POST, $url, [
|
||||||
|
'bank_question_form' => [
|
||||||
|
'question' => $bankQuestion->question,
|
||||||
|
'answers' => [
|
||||||
|
0 => ['text' => $texts[0], 'isRightAnswer' => '1', 'ordering' => '4'],
|
||||||
|
1 => ['text' => $texts[1], 'ordering' => '0'],
|
||||||
|
2 => ['text' => $texts[2], 'ordering' => '2'],
|
||||||
|
],
|
||||||
|
'_token' => $token,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$bankQuestion = $this->getBankQuestion('Wat at de Krtek als ontbijt?');
|
||||||
|
$reloadedAnswers = $bankQuestion->answers->toArray();
|
||||||
|
$this->assertCount(3, $reloadedAnswers);
|
||||||
|
$this->assertSame($texts[1], $reloadedAnswers[0]->text); // ordering=0 → first
|
||||||
|
$this->assertSame($texts[2], $reloadedAnswers[1]->text); // ordering=2 → second
|
||||||
|
$this->assertSame($texts[0], $reloadedAnswers[2]->text); // ordering=4 → third
|
||||||
|
}
|
||||||
|
|
||||||
public function testAddAndDeleteLabel(): void
|
public function testAddAndDeleteLabel(): void
|
||||||
{
|
{
|
||||||
$this->loginAsOwner();
|
$this->loginAsOwner();
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tvdt\Tests\Controller\Backoffice;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Tvdt\Controller\Backoffice\QuizQuestionController;
|
||||||
|
use Tvdt\Entity\Question;
|
||||||
|
use Tvdt\Entity\Quiz;
|
||||||
|
use Tvdt\Entity\User;
|
||||||
|
|
||||||
|
#[CoversClass(QuizQuestionController::class)]
|
||||||
|
final class QuizQuestionControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private KernelBrowser $client;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->client = self::createClient();
|
||||||
|
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loginAsOwner(): void
|
||||||
|
{
|
||||||
|
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'krtek-admin@example.org']);
|
||||||
|
$this->assertInstanceOf(User::class, $user);
|
||||||
|
$this->client->loginUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getQuizByName(string $name): Quiz
|
||||||
|
{
|
||||||
|
$quiz = $this->entityManager->getRepository(Quiz::class)->findOneBy(['name' => $name]);
|
||||||
|
$this->assertInstanceOf(Quiz::class, $quiz);
|
||||||
|
|
||||||
|
return $quiz;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditPreservesAnswerOrdering(): void
|
||||||
|
{
|
||||||
|
$this->loginAsOwner();
|
||||||
|
|
||||||
|
$quiz = $this->getQuizByName('Quiz 2');
|
||||||
|
$question = null;
|
||||||
|
foreach ($quiz->questions as $q) {
|
||||||
|
if ('Is de Krtek een man of een vrouw?' === $q->question) {
|
||||||
|
$question = $q;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Question::class, $question);
|
||||||
|
|
||||||
|
$answers = $question->answers->toArray();
|
||||||
|
$this->assertCount(2, $answers);
|
||||||
|
$firstText = $answers[0]->text;
|
||||||
|
$secondText = $answers[1]->text;
|
||||||
|
|
||||||
|
$url = \sprintf(
|
||||||
|
'/backoffice/season/krtek/quiz/%s/question/%s/edit',
|
||||||
|
$quiz->id,
|
||||||
|
$question->id,
|
||||||
|
);
|
||||||
|
|
||||||
|
$crawler = $this->client->request(Request::METHOD_GET, $url);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$token = (string) $crawler->filter('input[name="question_form[_token]"]')->attr('value');
|
||||||
|
|
||||||
|
// Submit with ordering values that invert which answer appears first on reload.
|
||||||
|
// The answer currently at index 0 ($firstText) gets ordering=7,
|
||||||
|
// the one at index 1 ($secondText) gets ordering=3.
|
||||||
|
// @OrderBy(['ordering' => 'ASC']) on Question::$answers will return
|
||||||
|
// $secondText (3) before $firstText (7) after flush+clear.
|
||||||
|
$this->client->request(Request::METHOD_POST, $url, [
|
||||||
|
'question_form' => [
|
||||||
|
'question' => $question->question,
|
||||||
|
'answers' => [
|
||||||
|
0 => ['text' => $firstText, 'ordering' => '7'],
|
||||||
|
1 => ['text' => $secondText, 'ordering' => '3'],
|
||||||
|
],
|
||||||
|
'_token' => $token,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseRedirects();
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$quiz = $this->getQuizByName('Quiz 2');
|
||||||
|
$reloadedQuestion = null;
|
||||||
|
foreach ($quiz->questions as $q) {
|
||||||
|
if ('Is de Krtek een man of een vrouw?' === $q->question) {
|
||||||
|
$reloadedQuestion = $q;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Question::class, $reloadedQuestion);
|
||||||
|
|
||||||
|
$reloadedAnswers = $reloadedQuestion->answers->toArray();
|
||||||
|
$this->assertSame(3, $reloadedAnswers[0]->ordering);
|
||||||
|
$this->assertSame($secondText, $reloadedAnswers[0]->text);
|
||||||
|
$this->assertSame(7, $reloadedAnswers[1]->ordering);
|
||||||
|
$this->assertSame($firstText, $reloadedAnswers[1]->text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReorderQuestionsWithinQuiz(): void
|
||||||
|
{
|
||||||
|
$this->loginAsOwner();
|
||||||
|
|
||||||
|
$quiz = $this->getQuizByName('Quiz 2');
|
||||||
|
$originalQuestions = $quiz->questions->toArray();
|
||||||
|
$this->assertGreaterThanOrEqual(3, \count($originalQuestions));
|
||||||
|
|
||||||
|
$originalFirstId = (string) $originalQuestions[0]->id;
|
||||||
|
$originalLastId = (string) $originalQuestions[\count($originalQuestions) - 1]->id;
|
||||||
|
|
||||||
|
$overviewUrl = \sprintf('/backoffice/season/krtek/quiz/%s/overview', $quiz->id);
|
||||||
|
$crawler = $this->client->request(Request::METHOD_GET, $overviewUrl);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
|
||||||
|
$csrfToken = $crawler->filter('[data-bo--question-list-csrf-value]')->attr('data-bo--question-list-csrf-value');
|
||||||
|
$this->assertNotEmpty($csrfToken);
|
||||||
|
|
||||||
|
$reversedIds = array_reverse(array_map(static fn (Question $q): string => (string) $q->id, $originalQuestions));
|
||||||
|
|
||||||
|
$reorderUrl = \sprintf('/backoffice/season/krtek/quiz/%s/questions/reorder', $quiz->id);
|
||||||
|
$this->client->request(Request::METHOD_POST, $reorderUrl, [
|
||||||
|
'_token' => $csrfToken,
|
||||||
|
'ordering' => $reversedIds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(204);
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$quiz = $this->getQuizByName('Quiz 2');
|
||||||
|
$reorderedQuestions = $quiz->questions->toArray();
|
||||||
|
|
||||||
|
$this->assertSame($originalLastId, (string) $reorderedQuestions[0]->id);
|
||||||
|
$this->assertSame($originalFirstId, (string) $reorderedQuestions[\count($reorderedQuestions) - 1]->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,10 +181,6 @@
|
|||||||
<source>Could not find candidate with name {name} in elimination.</source>
|
<source>Could not find candidate with name {name} in elimination.</source>
|
||||||
<target>Kon geen kandidaat vinden met de naam {name} in de eliminatie</target>
|
<target>Kon geen kandidaat vinden met de naam {name} in de eliminatie</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Cn0sav3" resname="Could not save order">
|
|
||||||
<source>Could not save order</source>
|
|
||||||
<target>Kon volgorde niet opslaan</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="0DvmToq" resname="Create a season">
|
<trans-unit id="0DvmToq" resname="Create a season">
|
||||||
<source>Create a season</source>
|
<source>Create a season</source>
|
||||||
<target>Maak een seizoen aan</target>
|
<target>Maak een seizoen aan</target>
|
||||||
@@ -263,7 +259,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="NXU7HO." resname="Error saving order">
|
<trans-unit id="NXU7HO." resname="Error saving order">
|
||||||
<source>Error saving order</source>
|
<source>Error saving order</source>
|
||||||
<target>Fout bij opslaan volgorde</target>
|
<target>Fout bij het opslaan van de volgorde</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bgWPQMg" resname="Export to XLSX">
|
<trans-unit id="bgWPQMg" resname="Export to XLSX">
|
||||||
<source>Export to XLSX</source>
|
<source>Export to XLSX</source>
|
||||||
@@ -393,6 +389,10 @@
|
|||||||
<source>No candidates</source>
|
<source>No candidates</source>
|
||||||
<target>Geen kandidaten</target>
|
<target>Geen kandidaten</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="WPJalKI" resname="No questions have been added to this quiz yet.">
|
||||||
|
<source>No questions have been added to this quiz yet.</source>
|
||||||
|
<target>Er zijn nog geen vragen aan deze test toegevoegd.</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="IsJa5UL" resname="No questions in the question bank yet">
|
<trans-unit id="IsJa5UL" resname="No questions in the question bank yet">
|
||||||
<source>No questions in the question bank yet</source>
|
<source>No questions in the question bank yet</source>
|
||||||
<target>Nog geen vragen in de vragenbank</target>
|
<target>Nog geen vragen in de vragenbank</target>
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="PywqOf4" resname="Owner(s)">
|
<trans-unit id="PywqOf4" resname="Owner(s)">
|
||||||
<source>Owner(s)</source>
|
<source>Owner(s)</source>
|
||||||
<target>Eigena(a)r(en)</target>
|
<target>Eigenaar/Eigenaren</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="GqmFSHc" resname="Password">
|
<trans-unit id="GqmFSHc" resname="Password">
|
||||||
<source>Password</source>
|
<source>Password</source>
|
||||||
@@ -573,6 +573,10 @@
|
|||||||
<source>Red</source>
|
<source>Red</source>
|
||||||
<target>Rood</target>
|
<target>Rood</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="plQzNQU" resname="Refresh the page to try again.">
|
||||||
|
<source>Refresh the page to try again.</source>
|
||||||
|
<target>Ververs de pagina om het opnieuw te proberen.</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="fGfBzt6" resname="Register">
|
<trans-unit id="fGfBzt6" resname="Register">
|
||||||
<source>Register</source>
|
<source>Register</source>
|
||||||
<target>Registreren</target>
|
<target>Registreren</target>
|
||||||
@@ -653,10 +657,6 @@
|
|||||||
<source>Sync latest changes to this quiz</source>
|
<source>Sync latest changes to this quiz</source>
|
||||||
<target>Laatste wijzigingen synchroniseren naar deze quiz</target>
|
<target>Laatste wijzigingen synchroniseren naar deze quiz</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="F3q1oXk" resname="The new question order could not be saved. Reordering has been disabled until you refresh the page.">
|
|
||||||
<source>The new question order could not be saved. Reordering has been disabled until you refresh the page.</source>
|
|
||||||
<target>De nieuwe volgorde van de vragen kon niet worden opgeslagen. Herordenen is uitgeschakeld totdat u de pagina vernieuwt.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="_z4el3Z" resname="The password fields must match.">
|
<trans-unit id="_z4el3Z" resname="The password fields must match.">
|
||||||
<source>The password fields must match.</source>
|
<source>The password fields must match.</source>
|
||||||
<target>De wachtwoorden moeten overeen komen.</target>
|
<target>De wachtwoorden moeten overeen komen.</target>
|
||||||
@@ -723,7 +723,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id=".j31AXY" resname="Toggle correct answer">
|
<trans-unit id=".j31AXY" resname="Toggle correct answer">
|
||||||
<source>Toggle correct answer</source>
|
<source>Toggle correct answer</source>
|
||||||
<target></target>
|
<target>Goed antwoord aan/uitzetten</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="XLYBGca" resname="Unassign">
|
<trans-unit id="XLYBGca" resname="Unassign">
|
||||||
<source>Unassign</source>
|
<source>Unassign</source>
|
||||||
|
|||||||
Reference in New Issue
Block a user