mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-08 00:20:15 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a75bc14638 | |||
|
e2558c70f5
|
|||
|
12d40b2602
|
|||
|
cb577d60d3
|
|||
|
3b6ff680e5
|
|||
|
4a87ac574d
|
|||
|
394b8c7d33
|
|||
|
64a09453e6
|
@@ -1,5 +1,3 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='Tvdt\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
MAILER_DSN=null://null
|
||||
MAILER_SENDER=test@example.org
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
- name: Load fixtures
|
||||
run: docker compose exec -T php bin/console -e test doctrine:fixtures:load --no-interaction --group=test
|
||||
- name: Run PHPUnit
|
||||
run: docker compose exec -T -e MAILER_DSN=null://null php vendor/bin/phpunit --log-junit var/phpunit/junit.xml
|
||||
run: docker compose exec -T php vendor/bin/phpunit --log-junit var/phpunit/junit.xml
|
||||
- name: Publish PHPUnit test results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@d9f48fc87bc235f7e214acf696ca5abc0a986f16 # v6
|
||||
@@ -260,8 +260,6 @@ jobs:
|
||||
compose.yaml
|
||||
compose.build.yaml
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=${{github.ref}}-devbuild
|
||||
*.cache-from=type=gha,scope=refs/heads/main-devbuild
|
||||
*.cache-from=type=gha,scope=${{github.ref}}
|
||||
*.cache-from=type=gha,scope=refs/heads/main
|
||||
*.cache-to=type=gha,scope=${{github.ref}},mode=max
|
||||
|
||||
Generated
-1
@@ -171,7 +171,6 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-deepclone" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/file-filter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/object-mapper" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
Generated
-1
@@ -205,7 +205,6 @@
|
||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||
<path value="$PROJECT_DIR$/vendor/martin-georgiev/postgresql-for-doctrine" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/object-mapper" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpInterpreters">
|
||||
|
||||
@@ -148,11 +148,6 @@ tests/
|
||||
- Coverage excluded from: `src/DataFixtures/`
|
||||
- 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
|
||||
- **PHP-CS-Fixer**: Symfony ruleset + risky rules enabled
|
||||
- Strict types declaration required
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.min.css';
|
||||
import './styles/backoffice.scss';
|
||||
import '@hotwired/turbo';
|
||||
import {session as turboSession} from '@hotwired/turbo';
|
||||
turboSession.drive = false;
|
||||
import './stimulus.js';
|
||||
import './bootstrap.js';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
@@ -6,30 +6,30 @@ export default class extends Controller {
|
||||
|
||||
connect() {
|
||||
this.index = this.collectionTarget.children.length;
|
||||
this._setupDrag();
|
||||
this._syncOrdering();
|
||||
|
||||
if (this.index === 0) {
|
||||
this.addItem();
|
||||
}
|
||||
|
||||
// `submit` fires on the ancestor <form>, which is outside this controller's
|
||||
// subtree. Stimulus data-action only works within the controller element, so
|
||||
// addEventListener on the form is the only option here.
|
||||
this._form = this.element.closest('form');
|
||||
if (this._form) {
|
||||
this._submitHandler = () => {
|
||||
this.collectionTarget.addEventListener('input', (e) => {
|
||||
if (e.target.type !== 'text') return;
|
||||
const item = e.target.closest('[data-collection-item]');
|
||||
const last = [...this.collectionTarget.children].at(-1);
|
||||
if (item && item === last && e.target.value.trim() !== '') {
|
||||
this.addItem();
|
||||
}
|
||||
});
|
||||
|
||||
const form = this.element.closest('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', () => {
|
||||
[...this.collectionTarget.children].forEach(item => {
|
||||
const input = item.querySelector('input[type="text"]');
|
||||
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,6 +38,7 @@ export default class extends Controller {
|
||||
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.index);
|
||||
const el = item.firstElementChild;
|
||||
this.collectionTarget.appendChild(el);
|
||||
this._makeDraggable(el);
|
||||
this.index++;
|
||||
this._syncOrdering();
|
||||
}
|
||||
@@ -70,61 +71,59 @@ export default class extends Controller {
|
||||
this._notifyChange();
|
||||
}
|
||||
|
||||
autoExpand(event) {
|
||||
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();
|
||||
}
|
||||
_notifyChange() {
|
||||
this.element.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
|
||||
// — drag-and-drop —
|
||||
|
||||
dragStart(event) {
|
||||
this._dragging = event.currentTarget.closest('[data-collection-item]');
|
||||
this._dragging.classList.add('opacity-50');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
_setupDrag() {
|
||||
[...this.collectionTarget.children].forEach(el => this._makeDraggable(el));
|
||||
}
|
||||
|
||||
dragEnd(event) {
|
||||
event.currentTarget.closest('[data-collection-item]').classList.remove('opacity-50');
|
||||
this._dragging = null;
|
||||
this.collectionTarget.querySelectorAll('[data-collection-item]').forEach(i =>
|
||||
i.classList.remove('border-top', 'border-bottom', 'border-primary'),
|
||||
);
|
||||
}
|
||||
_makeDraggable(el) {
|
||||
const handle = el.querySelector('[data-drag-handle]');
|
||||
if (!handle) return;
|
||||
|
||||
dragOver(event) {
|
||||
event.preventDefault();
|
||||
const el = event.currentTarget;
|
||||
if (!this._dragging || this._dragging === el) return;
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
const rect = el.getBoundingClientRect();
|
||||
const isBottom = event.clientY > rect.top + rect.height / 2;
|
||||
el.classList.toggle('border-top', !isBottom);
|
||||
el.classList.toggle('border-bottom', isBottom);
|
||||
el.classList.add('border-primary');
|
||||
}
|
||||
handle.setAttribute('draggable', 'true');
|
||||
|
||||
dragLeave(event) {
|
||||
event.currentTarget.classList.remove('border-top', 'border-bottom', 'border-primary');
|
||||
}
|
||||
handle.addEventListener('dragstart', (e) => {
|
||||
this._dragging = el;
|
||||
el.classList.add('opacity-50');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
|
||||
drop(event) {
|
||||
event.preventDefault();
|
||||
const el = event.currentTarget;
|
||||
el.classList.remove('border-top', 'border-bottom', 'border-primary');
|
||||
if (!this._dragging || this._dragging === el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const isBottom = event.clientY > rect.top + rect.height / 2;
|
||||
this.collectionTarget.insertBefore(this._dragging, isBottom ? el.nextSibling : el);
|
||||
this._syncOrdering();
|
||||
this._notifyChange();
|
||||
}
|
||||
handle.addEventListener('dragend', () => {
|
||||
this._dragging = null;
|
||||
el.classList.remove('opacity-50');
|
||||
this.collectionTarget.querySelectorAll('[data-collection-item]').forEach(i => i.classList.remove('border-top', 'border-bottom', 'border-primary'));
|
||||
});
|
||||
|
||||
_notifyChange() {
|
||||
this.element.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
el.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (!this._dragging || this._dragging === el) return;
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
const rect = el.getBoundingClientRect();
|
||||
const isBottom = e.clientY > rect.top + rect.height / 2;
|
||||
el.classList.toggle('border-top', !isBottom);
|
||||
el.classList.toggle('border-bottom', isBottom);
|
||||
el.classList.add('border-primary');
|
||||
});
|
||||
|
||||
el.addEventListener('dragleave', () => {
|
||||
el.classList.remove('border-top', 'border-bottom', 'border-primary');
|
||||
});
|
||||
|
||||
el.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
el.classList.remove('border-top', 'border-bottom', 'border-primary');
|
||||
if (!this._dragging || this._dragging === el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const isBottom = e.clientY > rect.top + rect.height / 2;
|
||||
this.collectionTarget.insertBefore(this._dragging, isBottom ? el.nextSibling : el);
|
||||
this._syncOrdering();
|
||||
this._notifyChange();
|
||||
});
|
||||
}
|
||||
|
||||
_syncOrdering() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Controller} from '@hotwired/stimulus';
|
||||
import {Modal} from 'bootstrap';
|
||||
import {visit} from '@hotwired/turbo';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['modal', 'frame'];
|
||||
@@ -12,36 +11,36 @@ export default class extends Controller {
|
||||
const titleEl = this.modalTarget.querySelector('.modal-title');
|
||||
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.removeAttribute('src');
|
||||
this.frameTarget.setAttribute('src', src);
|
||||
Modal.getOrCreateInstance(this.modalTarget).show();
|
||||
}
|
||||
|
||||
frameLoad() {
|
||||
this._bindDirty();
|
||||
}
|
||||
|
||||
frameSubmitEnd(event) {
|
||||
if (event.detail.success) {
|
||||
Modal.getOrCreateInstance(this.modalTarget).hide();
|
||||
visit(window.location.href);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
_bindDirty() {
|
||||
const modal = Modal.getOrCreateInstance(this.modalTarget);
|
||||
modal._config.backdrop = 'static';
|
||||
modal._config.keyboard = false;
|
||||
const markDirty = () => {
|
||||
modal._config.backdrop = 'static';
|
||||
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() {
|
||||
this._dirty = false;
|
||||
_resetDirty() {
|
||||
const modal = Modal.getOrCreateInstance(this.modalTarget);
|
||||
modal._config.backdrop = true;
|
||||
modal._config.keyboard = true;
|
||||
|
||||
@@ -1,67 +1,83 @@
|
||||
import {Controller} from '@hotwired/stimulus';
|
||||
import {Modal} from 'bootstrap';
|
||||
|
||||
const RETRY_DELAY_MS = 1000;
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['list', 'item', 'status'];
|
||||
static targets = ['list', 'item', 'status', 'noticeModal'];
|
||||
static values = {
|
||||
reorderUrl: String,
|
||||
csrf: String,
|
||||
canModify: Boolean,
|
||||
savedLabel: String,
|
||||
errorLabel: String,
|
||||
errorHint: String,
|
||||
};
|
||||
|
||||
connect() {
|
||||
this._locked = false;
|
||||
}
|
||||
|
||||
dragStart(event) {
|
||||
const item = event.currentTarget.closest('[data-bo--question-list-target="item"]');
|
||||
this._dragging = item;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
setTimeout(() => item.classList.add('opacity-50'), 0);
|
||||
}
|
||||
|
||||
dragEnd(event) {
|
||||
const item = event.currentTarget.closest('[data-bo--question-list-target="item"]');
|
||||
item.classList.remove('opacity-50');
|
||||
this._dragging = null;
|
||||
this._removePlaceholder();
|
||||
}
|
||||
|
||||
dragOver(event) {
|
||||
event.preventDefault();
|
||||
if (!this._dragging) return;
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
|
||||
const target = event.target.closest('[data-bo--question-list-target="item"]');
|
||||
if (!target || target === this._dragging) return;
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const insertBefore = event.clientY > rect.top + rect.height / 2 ? target.nextSibling : target;
|
||||
|
||||
if (!this._placeholder) {
|
||||
this._placeholder = document.createElement('div');
|
||||
this._placeholder.className = 'bg-primary rounded mb-2';
|
||||
this._placeholder.style.height = '3px';
|
||||
}
|
||||
|
||||
if (this._placeholder.nextSibling !== insertBefore) {
|
||||
this.listTarget.insertBefore(this._placeholder, insertBefore);
|
||||
if (this.canModifyValue) {
|
||||
this._setupDrag();
|
||||
}
|
||||
}
|
||||
|
||||
dragLeave(event) {
|
||||
if (!event.relatedTarget || !this.listTarget.contains(event.relatedTarget)) {
|
||||
_setupDrag() {
|
||||
this.itemTargets.forEach(el => {
|
||||
const handle = el.querySelector('[data-drag-handle]');
|
||||
if (!handle) return;
|
||||
|
||||
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', () => {
|
||||
el.classList.remove('opacity-50');
|
||||
this._dragging = null;
|
||||
this._removePlaceholder();
|
||||
});
|
||||
});
|
||||
|
||||
this.listTarget.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (!this._dragging) return;
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
const target = e.target.closest('[data-bo--question-list-target="item"]');
|
||||
if (!target || target === this._dragging) return;
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const insertBefore = e.clientY > rect.top + rect.height / 2 ? target.nextSibling : target;
|
||||
|
||||
if (!this._placeholder) {
|
||||
this._placeholder = document.createElement('div');
|
||||
this._placeholder.className = 'bg-primary rounded mb-2';
|
||||
this._placeholder.style.height = '3px';
|
||||
}
|
||||
|
||||
if (this._placeholder.nextSibling !== insertBefore) {
|
||||
this.listTarget.insertBefore(this._placeholder, insertBefore);
|
||||
}
|
||||
});
|
||||
|
||||
this.listTarget.addEventListener('dragleave', (e) => {
|
||||
if (!e.relatedTarget || !this.listTarget.contains(e.relatedTarget)) {
|
||||
this._removePlaceholder();
|
||||
}
|
||||
});
|
||||
|
||||
this.listTarget.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!this._dragging || !this._placeholder) return;
|
||||
this.listTarget.insertBefore(this._dragging, this._placeholder);
|
||||
this._removePlaceholder();
|
||||
}
|
||||
}
|
||||
|
||||
async drop(event) {
|
||||
event.preventDefault();
|
||||
if (!this._dragging || !this._placeholder || this._locked) return;
|
||||
this.listTarget.insertBefore(this._dragging, this._placeholder);
|
||||
this._removePlaceholder();
|
||||
await this._persistOrder();
|
||||
await this._persistOrder();
|
||||
});
|
||||
}
|
||||
|
||||
_removePlaceholder() {
|
||||
@@ -98,26 +114,40 @@ export default class extends Controller {
|
||||
if (numberEl) numberEl.textContent = String(i + 1);
|
||||
});
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const attempt = async () => {
|
||||
const res = await fetch(this.reorderUrlValue, {method: 'POST', body: params});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected response status: ${res.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await attempt();
|
||||
} catch {
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
||||
try {
|
||||
const res = await fetch(this.reorderUrlValue, {method: 'POST', body: params});
|
||||
if (res.ok) {
|
||||
this._setStatus('saved');
|
||||
return;
|
||||
}
|
||||
await attempt();
|
||||
} catch {
|
||||
// network error — retry on first attempt
|
||||
this._setStatus('error');
|
||||
this._lockReordering();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._locked = true;
|
||||
this._setStatus('error');
|
||||
this._setStatus('saved');
|
||||
}
|
||||
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-danger alert-dismissible mt-3';
|
||||
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>`;
|
||||
this.listTarget.after(alert);
|
||||
_lockReordering() {
|
||||
this._locked = true;
|
||||
this.itemTargets.forEach(el => {
|
||||
const handle = el.querySelector('[data-drag-handle]');
|
||||
if (handle) {
|
||||
handle.removeAttribute('draggable');
|
||||
handle.classList.add('opacity-25', 'pe-none');
|
||||
}
|
||||
});
|
||||
if (this.hasNoticeModalTarget) {
|
||||
Modal.getOrCreateInstance(this.noticeModalTarget).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"symfony/ux-turbo": "^3.1",
|
||||
"symfony/validator": "8.1.*",
|
||||
"symfony/yaml": "8.1.*",
|
||||
"symfonycasts/reset-password-bundle": "^1.25",
|
||||
"symfonycasts/sass-bundle": "^0.10",
|
||||
"symfonycasts/verify-email-bundle": "^1.18.0",
|
||||
"thecodingmachine/safe": "^3.4.0",
|
||||
|
||||
Generated
+39
-86
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "010a4456ebc1a8ebaf73c6db051d3d09",
|
||||
"content-hash": "7171824ca13f4df0801dfa5d7f58d6a0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
@@ -8281,54 +8281,6 @@
|
||||
],
|
||||
"time": "2026-06-09T11:06:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfonycasts/reset-password-bundle",
|
||||
"version": "v1.25.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SymfonyCasts/reset-password-bundle.git",
|
||||
"reference": "084aac1cc40ef75b26134c7967d1e423eeff72f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SymfonyCasts/reset-password-bundle/zipball/084aac1cc40ef75b26134c7967d1e423eeff72f4",
|
||||
"reference": "084aac1cc40ef75b26134c7967d1e423eeff72f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.10",
|
||||
"symfony/clock": "^6.3 | ^7.0 | ^8.0",
|
||||
"symfony/config": "^5.4 | ^6.0 | ^7.0 | ^8.0",
|
||||
"symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0 | ^8.0",
|
||||
"symfony/deprecation-contracts": "^2.2 | ^3.0",
|
||||
"symfony/http-kernel": "^5.4 | ^6.0 | ^7.0 | ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/annotations": "^1.0 | ^2.0",
|
||||
"doctrine/doctrine-bundle": "^2.13 | ^3.0",
|
||||
"doctrine/orm": "^2.20 | ^3.0",
|
||||
"symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0 | ^8.0",
|
||||
"symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0 | ^8.0",
|
||||
"symfony/process": "^6.4 | ^7.0 | ^8.0",
|
||||
"symfonycasts/internal-test-helpers": "dev-main"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SymfonyCasts\\Bundle\\ResetPassword\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Symfony bundle that adds password reset functionality.",
|
||||
"support": {
|
||||
"issues": "https://github.com/SymfonyCasts/reset-password-bundle/issues",
|
||||
"source": "https://github.com/SymfonyCasts/reset-password-bundle/tree/v1.25.0"
|
||||
},
|
||||
"time": "2026-03-26T10:16:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfonycasts/sass-bundle",
|
||||
"version": "v0.10.0",
|
||||
@@ -9573,19 +9525,20 @@
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.8.0",
|
||||
"version": "v5.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f"
|
||||
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
|
||||
"reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
|
||||
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"php": ">=7.4"
|
||||
@@ -9624,9 +9577,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0"
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
|
||||
},
|
||||
"time": "2026-07-04T14:30:18+00:00"
|
||||
"time": "2025-12-06T11:56:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
@@ -9796,11 +9749,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.5",
|
||||
"version": "2.2.4",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||
"reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/f0fe3fb03bb53ce68cc2416785b260e62226ec27",
|
||||
"reference": "f0fe3fb03bb53ce68cc2416785b260e62226ec27",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -9856,7 +9809,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-05T06:31:06+00:00"
|
||||
"time": "2026-07-03T07:00:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
@@ -9937,16 +9890,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-phpunit",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-phpunit.git",
|
||||
"reference": "f5dc20ff8082d02339b60cab68ec3eb0d859fb30"
|
||||
"reference": "c2f977551f0736d60467b3d754b2e0cf4e337b3f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f5dc20ff8082d02339b60cab68ec3eb0d859fb30",
|
||||
"reference": "f5dc20ff8082d02339b60cab68ec3eb0d859fb30",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/c2f977551f0736d60467b3d754b2e0cf4e337b3f",
|
||||
"reference": "c2f977551f0736d60467b3d754b2e0cf4e337b3f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -9989,9 +9942,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.18"
|
||||
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.17"
|
||||
},
|
||||
"time": "2026-07-04T12:16:09+00:00"
|
||||
"time": "2026-06-29T05:32:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
@@ -10069,16 +10022,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "14.2.3",
|
||||
"version": "14.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "82f6e49ff224e2cde923d74425e583a883910783"
|
||||
"reference": "10d7da3628a99289cdf4c662dd7f0d73f1baec83"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/82f6e49ff224e2cde923d74425e583a883910783",
|
||||
"reference": "82f6e49ff224e2cde923d74425e583a883910783",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/10d7da3628a99289cdf4c662dd7f0d73f1baec83",
|
||||
"reference": "10d7da3628a99289cdf4c662dd7f0d73f1baec83",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10086,7 +10039,7 @@
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^5.8.0",
|
||||
"nikic/php-parser": "^5.7.0",
|
||||
"php": ">=8.4",
|
||||
"phpunit/php-text-template": "^6.0",
|
||||
"sebastian/complexity": "^6.0",
|
||||
@@ -10097,7 +10050,7 @@
|
||||
"theseer/tokenizer": "^2.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^13.2.2"
|
||||
"phpunit/phpunit": "^13.2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcov": "PHP extension that provides line coverage",
|
||||
@@ -10135,7 +10088,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.3"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10155,7 +10108,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-06T15:04:02+00:00"
|
||||
"time": "2026-06-08T11:50:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
@@ -10452,24 +10405,24 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "13.2.3",
|
||||
"version": "13.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "d76d0e24225e587d6a5f0c6f6d9fef0d90712b54"
|
||||
"reference": "492c067e618de7b3c76105082c90f9d2833401b7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d76d0e24225e587d6a5f0c6f6d9fef0d90712b54",
|
||||
"reference": "d76d0e24225e587d6a5f0c6f6d9fef0d90712b54",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/492c067e618de7b3c76105082c90f9d2833401b7",
|
||||
"reference": "492c067e618de7b3c76105082c90f9d2833401b7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-filter": "*",
|
||||
"ext-json": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"myclabs/deep-copy": "^1.13.4",
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
@@ -10532,7 +10485,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.3"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10540,7 +10493,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-06T14:55:56+00:00"
|
||||
"time": "2026-06-29T13:36:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/cache",
|
||||
@@ -11070,16 +11023,16 @@
|
||||
},
|
||||
{
|
||||
"name": "rector/rector",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rectorphp/rector.git",
|
||||
"reference": "adaa18d7cd6b3c960967cfbc98c03efb3116ac0e"
|
||||
"reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/adaa18d7cd6b3c960967cfbc98c03efb3116ac0e",
|
||||
"reference": "adaa18d7cd6b3c960967cfbc98c03efb3116ac0e",
|
||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/49ff6339174bdbdf50b0b35ecbcff14a05ac9e24",
|
||||
"reference": "49ff6339174bdbdf50b0b35ecbcff14a05ac9e24",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11118,7 +11071,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rectorphp/rector/issues",
|
||||
"source": "https://github.com/rectorphp/rector/tree/2.5.4"
|
||||
"source": "https://github.com/rectorphp/rector/tree/2.5.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11126,7 +11079,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-06T12:41:46+00:00"
|
||||
"time": "2026-06-22T11:39:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
||||
@@ -15,7 +15,6 @@ use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
||||
use Symfony\UX\StimulusBundle\StimulusBundle;
|
||||
use Symfony\UX\Turbo\TurboBundle;
|
||||
use SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle;
|
||||
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
|
||||
use Symfonycasts\SassBundle\SymfonycastsSassBundle;
|
||||
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
|
||||
@@ -37,5 +36,4 @@ return [
|
||||
TurboBundle::class => ['all' => true],
|
||||
DAMADoctrineTestBundle::class => ['test' => true],
|
||||
StofDoctrineExtensionsBundle::class => ['all' => true],
|
||||
SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
symfonycasts_reset_password:
|
||||
request_password_repository: Tvdt\Repository\ResetPasswordRequestRepository
|
||||
Generated
-10
@@ -1490,12 +1490,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* skip_translation_on_load?: bool|Param, // Default: false
|
||||
* metadata_cache_pool?: scalar|Param|null, // Default: null
|
||||
* }
|
||||
* @psalm-type SymfonycastsResetPasswordConfig = array{
|
||||
* request_password_repository?: scalar|Param|null, // A class that implements ResetPasswordRequestRepositoryInterface - usually your ResetPasswordRequestRepository.
|
||||
* lifetime?: int|Param, // The length of time in seconds that a password reset request is valid for after it is created. // Default: 3600
|
||||
* throttle_limit?: int|Param, // Another password reset cannot be made faster than this throttle time in seconds. // Default: 3600
|
||||
* enable_garbage_collection?: bool|Param, // Enable/Disable automatic garbage collection. // Default: true
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1511,7 +1505,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* stof_doctrine_extensions?: StofDoctrineExtensionsConfig,
|
||||
* symfonycasts_reset_password?: SymfonycastsResetPasswordConfig,
|
||||
* "when@dev"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1530,7 +1523,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* stof_doctrine_extensions?: StofDoctrineExtensionsConfig,
|
||||
* symfonycasts_reset_password?: SymfonycastsResetPasswordConfig,
|
||||
* },
|
||||
* "when@prod"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1548,7 +1540,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* stimulus?: StimulusConfig,
|
||||
* turbo?: TurboConfig,
|
||||
* stof_doctrine_extensions?: StofDoctrineExtensionsConfig,
|
||||
* symfonycasts_reset_password?: SymfonycastsResetPasswordConfig,
|
||||
* },
|
||||
* "when@test"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1567,7 +1558,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* turbo?: TurboConfig,
|
||||
* dama_doctrine_test?: DamaDoctrineTestConfig,
|
||||
* stof_doctrine_extensions?: StofDoctrineExtensionsConfig,
|
||||
* symfonycasts_reset_password?: SymfonycastsResetPasswordConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
* imports?: ImportsConfig,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/** Auto-generated Migration: Please modify to your needs! */
|
||||
final class Version20260707205155 extends AbstractMigration
|
||||
{
|
||||
#[\Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE reset_password_request (id UUID NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, user_id UUID NOT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE INDEX IDX_7CE748AA76ED395 ON reset_password_request (user_id)');
|
||||
$this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE reset_password_request DROP CONSTRAINT FK_7CE748AA76ED395');
|
||||
$this->addSql('DROP TABLE reset_password_request');
|
||||
}
|
||||
}
|
||||
@@ -90,13 +90,11 @@ class QuestionBankController extends AbstractController
|
||||
|
||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||
|
||||
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, [
|
||||
'season' => $season,
|
||||
'action' => $this->generateUrl('tvdt_backoffice_question_bank_new', ['seasonCode' => $season->seasonCode]),
|
||||
]);
|
||||
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, ['season' => $season]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->applyAnswerOrdering($bankQuestion);
|
||||
$season->addBankQuestion($bankQuestion);
|
||||
$this->em->persist($bankQuestion);
|
||||
$this->em->flush();
|
||||
@@ -140,13 +138,7 @@ class QuestionBankController extends AbstractController
|
||||
|
||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||
|
||||
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, [
|
||||
'season' => $season,
|
||||
'action' => $this->generateUrl('tvdt_backoffice_question_bank_edit', [
|
||||
'seasonCode' => $season->seasonCode,
|
||||
'bankQuestion' => $bankQuestion->id,
|
||||
]),
|
||||
]);
|
||||
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, ['season' => $season]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -389,6 +381,14 @@ 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
|
||||
{
|
||||
$pendingNames = [];
|
||||
|
||||
@@ -46,16 +46,11 @@ class QuizQuestionController extends AbstractController
|
||||
|
||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||
|
||||
$form = $this->createForm(QuestionFormType::class, $question, [
|
||||
'action' => $this->generateUrl('tvdt_backoffice_quiz_question_edit', [
|
||||
'seasonCode' => $season->seasonCode,
|
||||
'quiz' => $quiz->id,
|
||||
'question' => $question->id,
|
||||
]),
|
||||
]);
|
||||
$form = $this->createForm(QuestionFormType::class, $question);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->applyAnswerOrdering($question);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Question updated'));
|
||||
@@ -88,7 +83,6 @@ class QuizQuestionController extends AbstractController
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
#[Route(
|
||||
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/question/{question}/view',
|
||||
name: 'tvdt_backoffice_quiz_question_view',
|
||||
@@ -133,10 +127,6 @@ class QuizQuestionController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
if (\count(array_unique($ordering)) !== \count($questionsById)) {
|
||||
throw new BadRequestHttpException('Ordering must include every question exactly once');
|
||||
}
|
||||
|
||||
$position = 1;
|
||||
foreach ($ordering as $questionId) {
|
||||
$questionsById[$questionId]->ordering = $position++;
|
||||
@@ -146,4 +136,12 @@ class QuizQuestionController extends AbstractController
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
private function applyAnswerOrdering(Question $question): void
|
||||
{
|
||||
$ordering = 1;
|
||||
foreach ($question->answers as $answer) {
|
||||
$answer->ordering = $ordering++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Controller;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken;
|
||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
||||
use Tvdt\Entity\User;
|
||||
use Tvdt\Enum\FlashType;
|
||||
use Tvdt\Form\ChangePasswordFormType;
|
||||
use Tvdt\Form\ResetPasswordRequestFormType;
|
||||
|
||||
final class ResetPasswordController extends AbstractController
|
||||
{
|
||||
use ResetPasswordControllerTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly ResetPasswordHelperInterface $resetPasswordHelper,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
) {}
|
||||
|
||||
#[Route('/reset-password', name: 'tvdt_forgot_password_request')]
|
||||
public function request(Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(ResetPasswordRequestFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var string $email */
|
||||
$email = $form->get('email')->getData();
|
||||
|
||||
return $this->processSendingPasswordResetEmail($email, $this->mailer, $this->translator);
|
||||
}
|
||||
|
||||
return $this->render('reset_password/request.html.twig', [
|
||||
'requestForm' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/reset-password/check-email', name: 'tvdt_check_email')]
|
||||
public function checkEmail(): Response
|
||||
{
|
||||
if (!($resetToken = $this->getTokenObjectFromSession()) instanceof ResetPasswordToken) {
|
||||
$resetToken = $this->resetPasswordHelper->generateFakeResetToken();
|
||||
}
|
||||
|
||||
return $this->render('reset_password/check_email.html.twig', [
|
||||
'resetToken' => $resetToken,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/reset-password/reset/{token}', name: 'tvdt_reset_password')]
|
||||
public function reset(Request $request, ?string $token = null): Response
|
||||
{
|
||||
if ($token) {
|
||||
$this->storeTokenInSession($token);
|
||||
|
||||
return $this->redirectToRoute('tvdt_reset_password');
|
||||
}
|
||||
|
||||
$token = $this->getTokenFromSession();
|
||||
if (null === $token) {
|
||||
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var User $user */
|
||||
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
|
||||
} catch (ResetPasswordExceptionInterface $resetPasswordException) {
|
||||
$this->addFlash(FlashType::Danger->value, \sprintf(
|
||||
'%s - %s',
|
||||
$this->translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
|
||||
$this->translator->trans($resetPasswordException->getReason(), [], 'ResetPasswordBundle'),
|
||||
));
|
||||
|
||||
return $this->redirectToRoute('tvdt_forgot_password_request');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ChangePasswordFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->resetPasswordHelper->removeResetRequest($token);
|
||||
|
||||
/** @var string $plainPassword */
|
||||
$plainPassword = $form->get('plainPassword')->getData();
|
||||
|
||||
$user->password = $this->passwordHasher->hashPassword($user, $plainPassword);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->cleanSessionAfterReset();
|
||||
|
||||
return $this->redirectToRoute('tvdt_backoffice_index');
|
||||
}
|
||||
|
||||
return $this->render('reset_password/reset.html.twig', [
|
||||
'resetForm' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer, TranslatorInterface $translator): RedirectResponse
|
||||
{
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy([
|
||||
'email' => $emailFormData,
|
||||
]);
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $this->redirectToRoute('tvdt_check_email');
|
||||
}
|
||||
|
||||
try {
|
||||
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
|
||||
} catch (ResetPasswordExceptionInterface) {
|
||||
return $this->redirectToRoute('tvdt_check_email');
|
||||
}
|
||||
|
||||
$email = new TemplatedEmail()
|
||||
->from(new Address('info@tijdvoordetest.nl', 'Tijd voor de Test'))
|
||||
->to($user->getUserIdentifier())
|
||||
->subject($translator->trans('Your password reset request'))
|
||||
->htmlTemplate('reset_password/email.html.twig')
|
||||
->context([
|
||||
'resetToken' => $resetToken,
|
||||
]);
|
||||
|
||||
$mailer->send($email);
|
||||
|
||||
$this->setTokenObjectInSession($resetToken);
|
||||
|
||||
return $this->redirectToRoute('tvdt_check_email');
|
||||
}
|
||||
}
|
||||
@@ -54,13 +54,6 @@ class Question implements \Stringable
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeAnswer(Answer $answer): static
|
||||
{
|
||||
$this->answers->removeElement($answer);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->question ?? '';
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
|
||||
use Tvdt\Repository\ResetPasswordRequestRepository;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
|
||||
class ResetPasswordRequest implements ResetPasswordRequestInterface
|
||||
{
|
||||
use ResetPasswordRequestTrait;
|
||||
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
#[ORM\Id]
|
||||
public private(set) Uuid $id;
|
||||
|
||||
public function __construct(#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\ManyToOne]
|
||||
private User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
|
||||
{
|
||||
$this->initialize($expiresAt, $selector, $hashedToken);
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,6 @@ use Tvdt\Repository\UserRepository;
|
||||
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
public const int PASSWORD_MIN_LENGTH = 8;
|
||||
|
||||
public const int PASSWORD_MAX_LENGTH = 4096;
|
||||
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
|
||||
@@ -43,7 +43,6 @@ class BankQuestionFormType extends AbstractType
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choice_attr' => static fn (QuestionLabel $label): array => ['data-colour' => $label->colour->value],
|
||||
'query_builder' => static fn (QuestionLabelRepository $repository): QueryBuilder => $repository
|
||||
->createQueryBuilder('l')
|
||||
->where('l.season = :season')
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Tvdt\Entity\User;
|
||||
|
||||
/** @extends AbstractType<array{plainPassword: string}> */
|
||||
final class ChangePasswordFormType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('plainPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'options' => [
|
||||
'attr' => ['autocomplete' => 'new-password'],
|
||||
],
|
||||
'first_options' => [
|
||||
'label' => $this->translator->trans('New password'),
|
||||
'constraints' => [
|
||||
new NotBlank(message: 'Please enter a password'),
|
||||
new Length(
|
||||
min: User::PASSWORD_MIN_LENGTH,
|
||||
max: User::PASSWORD_MAX_LENGTH,
|
||||
minMessage: 'Your password should be at least {{ limit }} characters',
|
||||
),
|
||||
],
|
||||
],
|
||||
'second_options' => [
|
||||
'label' => $this->translator->trans('Repeat Password'),
|
||||
],
|
||||
'invalid_message' => $this->translator->trans('The password fields must match.'),
|
||||
'mapped' => false,
|
||||
'translation_domain' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([]);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class RegistrationFormType extends AbstractType
|
||||
'mapped' => false,
|
||||
'constraints' => [
|
||||
new NotBlank(message: 'Please enter a password'),
|
||||
new Length(min: User::PASSWORD_MIN_LENGTH, max: User::PASSWORD_MAX_LENGTH, minMessage: 'Your password should be at least {{ limit }} characters'),
|
||||
new Length(min: 8, max: 4096, minMessage: 'Your password should be at least {{ limit }} characters'),
|
||||
],
|
||||
'translation_domain' => false,
|
||||
])
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/** @extends AbstractType<array{email: string}> */
|
||||
final class ResetPasswordRequestFormType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'label' => $this->translator->trans('Email'),
|
||||
'attr' => ['autocomplete' => 'email'],
|
||||
'translation_domain' => false,
|
||||
'constraints' => [
|
||||
new NotBlank(message: 'Please enter your email'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([]);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Repository;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
|
||||
use Tvdt\Entity\ResetPasswordRequest;
|
||||
use Tvdt\Entity\User;
|
||||
|
||||
/** @extends ServiceEntityRepository<ResetPasswordRequest> */
|
||||
final class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface
|
||||
{
|
||||
use ResetPasswordRequestRepositoryTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ResetPasswordRequest::class);
|
||||
}
|
||||
|
||||
public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface
|
||||
{
|
||||
\assert($user instanceof User);
|
||||
|
||||
return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
|
||||
}
|
||||
}
|
||||
@@ -356,18 +356,6 @@
|
||||
"config/routes/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"symfonycasts/reset-password-bundle": {
|
||||
"version": "1.25",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "97c1627c0384534997ae1047b93be517ca16de43"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/reset_password.yaml"
|
||||
]
|
||||
},
|
||||
"symfonycasts/sass-bundle": {
|
||||
"version": "v0.8.2"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{% block body %}
|
||||
<form method="post">
|
||||
<h3 class="mb-3">{{ 'Please sign in'|trans }}</h3>
|
||||
<h1 class="py-2 h3 mb-3 font-weight-normal">{{ 'Please sign in'|trans }}</h1>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{ 'Email'|trans }}</label>
|
||||
<input type="email" value="{{ last_username }}" name="_username" id="username" class="form-control"
|
||||
@@ -31,7 +31,5 @@
|
||||
</button>
|
||||
<a href="{{ path('tvdt_register') }}"
|
||||
class="btn btn-link">{{ 'Create an account'|trans }}</a>
|
||||
<a href="{{ path('tvdt_forgot_password_request') }}"
|
||||
class="btn btn-link">{{ 'Forgot your password?'|trans }}</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
{% macro answer_row(answerForm) %}
|
||||
<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">
|
||||
<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 }}"
|
||||
draggable="true"
|
||||
data-action="dragstart->bo--form-collection#dragStart dragend->bo--form-collection#dragEnd"><i class="bi bi-grip-vertical"></i></span>
|
||||
<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"
|
||||
<button type="button" tabindex="-1"
|
||||
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'">
|
||||
|
||||
@@ -3,30 +3,13 @@
|
||||
{{ form_start(form, {attr: {novalidate: 'novalidate'}}) }}
|
||||
{{ form_row(form.question) }}
|
||||
{{ form_row(form.reusable) }}
|
||||
<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>
|
||||
{{ 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"
|
||||
data-action="input->bo--form-collection#autoExpand">
|
||||
<div data-bo--form-collection-target="collection">
|
||||
{% for answerForm in form.answers %}
|
||||
{{ macros.answer_row(answerForm) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -5,29 +5,12 @@
|
||||
<div class="modal-body">
|
||||
{{ form_row(form.question) }}
|
||||
{{ form_row(form.reusable) }}
|
||||
<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>
|
||||
{{ 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"
|
||||
data-action="input->bo--form-collection#autoExpand">
|
||||
<div data-bo--form-collection-target="collection">
|
||||
{% for answerForm in form.answers %}
|
||||
{{ macros.answer_row(answerForm) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
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"
|
||||
data-action="input->bo--form-collection#autoExpand">
|
||||
<div data-bo--form-collection-target="collection">
|
||||
{% for answerForm in form.answers %}
|
||||
{{ macros.answer_row(answerForm) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
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"
|
||||
data-action="input->bo--form-collection#autoExpand">
|
||||
<div data-bo--form-collection-target="collection">
|
||||
{% for answerForm in form.answers %}
|
||||
{{ macros.answer_row(answerForm) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -86,20 +86,19 @@
|
||||
</div>
|
||||
|
||||
<div data-controller="bo--question-list bo--modal"
|
||||
data-action="turbo:submit-end->bo--modal#frameSubmitEnd"
|
||||
data-action="turbo:frame-load->bo--modal#frameLoad 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-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-error-label-value="{{ 'Error saving order'|trans }}"
|
||||
data-bo--question-list-error-hint-value="{{ 'Refresh the page to try again.'|trans }}">
|
||||
data-bo--question-list-error-label-value="{{ 'Error saving order'|trans }}">
|
||||
|
||||
<h4 class="mb-3 d-flex align-items-center gap-2">
|
||||
{{ 'Questions'|trans }}
|
||||
<span class="badge d-none fw-normal" style="font-size:.7rem;vertical-align:baseline" data-bo--question-list-target="status"></span>
|
||||
</h4>
|
||||
|
||||
<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 %}>
|
||||
<div data-bo--question-list-target="list">
|
||||
{%~ for question in quiz.questions ~%}
|
||||
<div class="card mb-2"
|
||||
data-bo--question-list-target="item"
|
||||
@@ -107,9 +106,7 @@
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if is_granted('QUIZ_MODIFY_CONTENT', question) %}
|
||||
<span class="text-muted" style="cursor:grab"
|
||||
draggable="true"
|
||||
data-action="dragstart->bo--question-list#dragStart dragend->bo--question-list#dragEnd">
|
||||
<span class="text-muted" style="cursor:grab" data-drag-handle>
|
||||
<i class="bi bi-grip-vertical"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -138,13 +135,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ 'No questions have been added to this quiz yet.'|trans }}
|
||||
{{ 'EMPTY'|trans }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="modal fade" tabindex="-1"
|
||||
data-bo--modal-target="modal"
|
||||
data-action="hidden.bs.modal->bo--modal#resetDirty"
|
||||
aria-labelledby="questionEditModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
@@ -152,9 +148,26 @@
|
||||
<h5 class="modal-title" id="questionEditModalLabel">{{ 'Edit question'|trans }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<turbo-frame id="question-modal-frame"
|
||||
data-bo--modal-target="frame"
|
||||
data-action="input->bo--modal#markDirty change->bo--modal#markDirty"></turbo-frame>
|
||||
<turbo-frame id="question-modal-frame" data-bo--modal-target="frame"></turbo-frame>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block title %}{{ 'Register'|trans }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h3 class="mb-3">{{ 'Register'|trans }}</h3>
|
||||
<h3>{{ 'Register'|trans }}</h3>
|
||||
|
||||
{{ form_errors(registrationForm) }}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-12" data-controller="bo--modal"
|
||||
data-action="turbo:submit-end->bo--modal#frameSubmitEnd">
|
||||
data-action="turbo:frame-load->bo--modal#frameLoad turbo:submit-end->bo--modal#frameSubmitEnd">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
data-action="click->bo--modal#open"
|
||||
@@ -129,7 +129,6 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-action="click->bo--modal#open"
|
||||
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>
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal"
|
||||
data-bs-target="#deleteBankQuestion-{{ bankQuestion.id }}"
|
||||
@@ -173,7 +172,6 @@
|
||||
</table>
|
||||
<div class="modal fade" tabindex="-1"
|
||||
data-bo--modal-target="modal"
|
||||
data-action="hidden.bs.modal->bo--modal#resetDirty"
|
||||
aria-labelledby="bankQuestionEditModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
@@ -181,9 +179,7 @@
|
||||
<h5 class="modal-title" id="bankQuestionEditModalLabel">{{ 'Edit question'|trans }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<turbo-frame id="bank-question-modal-frame"
|
||||
data-bo--modal-target="frame"
|
||||
data-action="input->bo--modal#markDirty change->bo--modal#markDirty"></turbo-frame>
|
||||
<turbo-frame id="bank-question-modal-frame" data-bo--modal-target="frame"></turbo-frame>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
|
||||
{% block title %}E-mail verstuurd{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h3 class="mb-3">{{ 'Check your email'|trans }}</h3>
|
||||
|
||||
<p>
|
||||
{{ 'If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.'|trans }}
|
||||
{{ 'This link will expire in %count%.'|trans({'%count%': resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle')}) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ 'If you don\'t receive an email please check your spam folder or'|trans }}
|
||||
<a href="{{ path('tvdt_forgot_password_request') }}">{{ 'try again'|trans }}</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -1,9 +0,0 @@
|
||||
<h1>Hi!</h1>
|
||||
|
||||
<p>To reset your password, please visit the following link</p>
|
||||
|
||||
<a href="{{ url('tvdt_reset_password', {token: resetToken.token}) }}">{{ url('tvdt_reset_password', {token: resetToken.token}) }}</a>
|
||||
|
||||
<p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
|
||||
|
||||
<p>Cheers!</p>
|
||||
@@ -1,18 +0,0 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
|
||||
{% block title %}Wachtwoord vergeten{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h3 class="mb-3">{{ 'Reset your password'|trans }}</h3>
|
||||
|
||||
{{ form_start(requestForm) }}
|
||||
{{ form_row(requestForm.email) }}
|
||||
|
||||
<small class="d-block mb-3">
|
||||
{{ 'Enter your email address, and we will send you a link to reset your password.'|trans }}
|
||||
</small>
|
||||
|
||||
<button class="btn btn-primary" type="submit">{{ 'Send password reset email'|trans }}</button>
|
||||
<a href="{{ path('tvdt_login_login') }}" class="btn btn-link">{{ 'Back to login'|trans }}</a>
|
||||
{{ form_end(requestForm) }}
|
||||
{% endblock %}
|
||||
@@ -1,12 +0,0 @@
|
||||
{% extends 'backoffice/base.html.twig' %}
|
||||
|
||||
{% block title %}Nieuw wachtwoord instellen{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h3 class="mb-3">{{ 'Reset your password'|trans }}</h3>
|
||||
|
||||
{{ form_start(resetForm) }}
|
||||
{{ form_row(resetForm.plainPassword) }}
|
||||
<button class="btn btn-primary" type="submit">{{ 'Reset password'|trans }}</button>
|
||||
{{ form_end(resetForm) }}
|
||||
{% endblock %}
|
||||
@@ -10,7 +10,6 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Tvdt\Controller\Backoffice\QuestionBankController;
|
||||
use Tvdt\Entity\BankAnswer;
|
||||
use Tvdt\Entity\BankQuestion;
|
||||
use Tvdt\Entity\Question;
|
||||
use Tvdt\Entity\QuestionLabel;
|
||||
@@ -299,79 +298,6 @@ final class QuestionBankControllerTest extends WebTestCase
|
||||
$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
|
||||
{
|
||||
$this->loginAsOwner();
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Tests\Controller;
|
||||
|
||||
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 Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
||||
use Tvdt\Controller\ResetPasswordController;
|
||||
use Tvdt\Entity\User;
|
||||
|
||||
#[CoversClass(ResetPasswordController::class)]
|
||||
final class ResetPasswordControllerTest extends WebTestCase
|
||||
{
|
||||
private KernelBrowser $client;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->client = self::createClient();
|
||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
public function testRequestPageLoads(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/reset-password');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSelectorExists('form');
|
||||
}
|
||||
|
||||
public function testRequestWithUnknownEmailRedirectsToCheckEmail(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/reset-password');
|
||||
$form = $this->client->getCrawler()->filter('form')->form([
|
||||
'reset_password_request_form[email]' => 'unknown@example.org',
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseRedirects('/reset-password/check-email');
|
||||
}
|
||||
|
||||
public function testRequestWithKnownEmailRedirectsToCheckEmail(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/reset-password');
|
||||
$form = $this->client->getCrawler()->filter('form')->form([
|
||||
'reset_password_request_form[email]' => 'test@example.org',
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseRedirects('/reset-password/check-email');
|
||||
}
|
||||
|
||||
public function testCheckEmailPageLoads(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/reset-password/check-email');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testResetWithInvalidTokenRedirectsToRequest(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/reset-password/reset/invalidtoken');
|
||||
$this->client->followRedirect();
|
||||
|
||||
$this->assertResponseRedirects('/reset-password');
|
||||
}
|
||||
|
||||
public function testFullResetFlow(): void
|
||||
{
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'test@example.org']);
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
|
||||
/** @var ResetPasswordHelperInterface $helper */
|
||||
$helper = self::getContainer()->get(ResetPasswordHelperInterface::class);
|
||||
$resetToken = $helper->generateResetToken($user);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/reset-password/reset/'.$resetToken->getToken());
|
||||
$this->assertResponseRedirects('/reset-password/reset');
|
||||
|
||||
$this->client->followRedirect();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$form = $this->client->getCrawler()->filter('form')->form([
|
||||
'change_password_form[plainPassword][first]' => 'NewPass123!',
|
||||
'change_password_form[plainPassword][second]' => 'NewPass123!',
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseRedirects('/backoffice/');
|
||||
|
||||
$this->entityManager->clear();
|
||||
$updatedUser = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'test@example.org']);
|
||||
$this->assertInstanceOf(User::class, $updatedUser);
|
||||
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
$this->assertTrue($hasher->isPasswordValid($updatedUser, 'NewPass123!'));
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="nl" target-language="nl" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="E69bPZ6" resname="%count% day|%count% days">
|
||||
<source>%count% day|%count% days</source>
|
||||
<target>%count% dag|%count% dagen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="H3jnVKg" resname="%count% hour|%count% hours">
|
||||
<source>%count% hour|%count% hours</source>
|
||||
<target>%count% uur|%count% uren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="30tji6_" resname="%count% minute|%count% minutes">
|
||||
<source>%count% minute|%count% minutes</source>
|
||||
<target>%count% minuut|%count% minuten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="NCe6niW" resname="%count% month|%count% months">
|
||||
<source>%count% month|%count% months</source>
|
||||
<target>%count% maand|%count% maanden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="U_NoHhq" resname="%count% year|%count% years">
|
||||
<source>%count% year|%count% years</source>
|
||||
<target>%count% jaar|%count% jaar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="NUQGnNx" resname="Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service.">
|
||||
<source>Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service.</source>
|
||||
<target>Update de request_password_repository waarde in config/packages/reset_password.yaml met een verwijzing naar je "request password repository" service.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="XyawDv9" resname="The link in your email is expired. Please try to reset your password again.">
|
||||
<source>The link in your email is expired. Please try to reset your password again.</source>
|
||||
<target>De link in je e-mail is verlopen. Probeer opnieuw je wachtwoord te herstellen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="RPp.BKQ" resname="The reset password link is invalid. Please try to reset your password again.">
|
||||
<source>The reset password link is invalid. Please try to reset your password again.</source>
|
||||
<target>De link om je wachtwoord te herstellen is niet geldig. Probeer opnieuw je wachtwoord te herstellen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id=".BeFwl." resname="There was a problem handling your password reset request">
|
||||
<source>There was a problem handling your password reset request</source>
|
||||
<target>Er is een probleem opgetreden bij het afhandelen van het verzoek om je wachtwoord te herstellen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ZeI3pSX" resname="There was a problem validating your password reset request">
|
||||
<source>There was a problem validating your password reset request</source>
|
||||
<target>Er is een probleem opgetreden bij het valideren van het verzoek om je wachtwoord te herstellen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IZsa0iw" resname="You have already requested a reset password email. Please check your email or try again soon.">
|
||||
<source>You have already requested a reset password email. Please check your email or try again soon.</source>
|
||||
<target>Je hebt al een verzoek ingediend voor een e-mail om je wachtwoord te herstellen. Controleer of je een e-mail hebt ontvangen of probeer het later nog eens.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -109,10 +109,6 @@
|
||||
<source>Back</source>
|
||||
<target>Terug</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mR7n673" resname="Back to login">
|
||||
<source>Back to login</source>
|
||||
<target>Terug naar inloggen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="JUdglpF" resname="Backoffice">
|
||||
<source>Backoffice</source>
|
||||
<target>Backoffice</target>
|
||||
@@ -149,10 +145,6 @@
|
||||
<source>Candidates</source>
|
||||
<target>Kandidaten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="o6WwCao" resname="Check your email">
|
||||
<source>Check your email</source>
|
||||
<target>Controleer je e-mail</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="J1c2y63" resname="Clear Quiz...">
|
||||
<source>Clear Quiz...</source>
|
||||
<target>Test leegmaken...</target>
|
||||
@@ -189,6 +181,10 @@
|
||||
<source>Could not find candidate with name {name} in elimination.</source>
|
||||
<target>Kon geen kandidaat vinden met de naam {name} in de eliminatie</target>
|
||||
</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">
|
||||
<source>Create a season</source>
|
||||
<target>Maak een seizoen aan</target>
|
||||
@@ -257,10 +253,6 @@
|
||||
<source>Enter name</source>
|
||||
<target>Voer een naam in</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ifotYH6" resname="Enter your email address, and we will send you a link to reset your password.">
|
||||
<source>Enter your email address, and we will send you a link to reset your password.</source>
|
||||
<target>Voer je e-mailadres in en we sturen je een link om je wachtwoord te herstellen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="RnI7jJT" resname="Enter your name">
|
||||
<source>Enter your name</source>
|
||||
<target>Voer je naam in</target>
|
||||
@@ -271,7 +263,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="NXU7HO." resname="Error saving order">
|
||||
<source>Error saving order</source>
|
||||
<target>Fout bij het opslaan van de volgorde</target>
|
||||
<target>Fout bij opslaan volgorde</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bgWPQMg" resname="Export to XLSX">
|
||||
<source>Export to XLSX</source>
|
||||
@@ -285,10 +277,6 @@
|
||||
<source>Finalized</source>
|
||||
<target>Afgerond</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Fztxgjr" resname="Forgot your password?">
|
||||
<source>Forgot your password?</source>
|
||||
<target>Wachtwoord vergeten?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="MebBrmp" resname="Gray">
|
||||
<source>Gray</source>
|
||||
<target>Grijs</target>
|
||||
@@ -317,14 +305,6 @@
|
||||
<source>Home</source>
|
||||
<target>Home</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="NJn0f4_" resname="If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.">
|
||||
<source>If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.</source>
|
||||
<target>Als er een account met dit e-mailadres bestaat, is er zojuist een e-mail verstuurd met een link om je wachtwoord te herstellen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bDPZm0a" resname="If you don't receive an email please check your spam folder or">
|
||||
<source>If you don't receive an email please check your spam folder or</source>
|
||||
<target>Als je geen e-mail ontvangt, controleer dan je spammap of</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="oPzjCWg" resname="Import">
|
||||
<source>Import</source>
|
||||
<target>Importeren</target>
|
||||
@@ -397,10 +377,6 @@
|
||||
<source>New label</source>
|
||||
<target>Nieuw label</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zm.jZiK" resname="New password">
|
||||
<source>New password</source>
|
||||
<target>Nieuw wachtwoord</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="gefhnBC" resname="Next">
|
||||
<source>Next</source>
|
||||
<target>Volgende</target>
|
||||
@@ -417,10 +393,6 @@
|
||||
<source>No candidates</source>
|
||||
<target>Geen kandidaten</target>
|
||||
</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">
|
||||
<source>No questions in the question bank yet</source>
|
||||
<target>Nog geen vragen in de vragenbank</target>
|
||||
@@ -455,7 +427,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="PywqOf4" resname="Owner(s)">
|
||||
<source>Owner(s)</source>
|
||||
<target>Eigenaar/Eigenaren</target>
|
||||
<target>Eigena(a)r(en)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="GqmFSHc" resname="Password">
|
||||
<source>Password</source>
|
||||
@@ -601,10 +573,6 @@
|
||||
<source>Red</source>
|
||||
<target>Rood</target>
|
||||
</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">
|
||||
<source>Register</source>
|
||||
<target>Registreren</target>
|
||||
@@ -621,14 +589,6 @@
|
||||
<source>Repeat Password</source>
|
||||
<target>Herhaal wachtwoord</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="9JCvQkt" resname="Reset password">
|
||||
<source>Reset password</source>
|
||||
<target>Wachtwoord herstellen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eyayNGN" resname="Reset your password">
|
||||
<source>Reset your password</source>
|
||||
<target>Wachtwoord herstellen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7UvBPrb" resname="Results & Elimination">
|
||||
<source>Results & Elimination</source>
|
||||
<target><![CDATA[Resultaat & Eliminatie]]></target>
|
||||
@@ -665,10 +625,6 @@
|
||||
<source>Seasons</source>
|
||||
<target>Seizoenen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5XiCCZa" resname="Send password reset email">
|
||||
<source>Send password reset email</source>
|
||||
<target>Stuur herstel-e-mail</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="VXFwlwn" resname="Settings">
|
||||
<source>Settings</source>
|
||||
<target>Instellingen</target>
|
||||
@@ -697,6 +653,10 @@
|
||||
<source>Sync latest changes to this quiz</source>
|
||||
<target>Laatste wijzigingen synchroniseren naar deze quiz</target>
|
||||
</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.">
|
||||
<source>The password fields must match.</source>
|
||||
<target>De wachtwoorden moeten overeen komen.</target>
|
||||
@@ -729,10 +689,6 @@
|
||||
<source>There is no active quiz</source>
|
||||
<target>Er is geen test actief</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YueaA2f" resname="This link will expire in %count%.">
|
||||
<source>This link will expire in %count%.</source>
|
||||
<target>Deze link verloopt over %count%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="o.FTilS" resname="This question cannot be deleted because it is used in a locked or active quiz">
|
||||
<source>This question cannot be deleted because it is used in a locked or active quiz</source>
|
||||
<target>Deze vraag kan niet verwijderd worden omdat die gebruik wordt in een vergrendelde of actieve test</target>
|
||||
@@ -767,7 +723,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id=".j31AXY" resname="Toggle correct answer">
|
||||
<source>Toggle correct answer</source>
|
||||
<target>Goed antwoord aan/uitzetten</target>
|
||||
<target></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="XLYBGca" resname="Unassign">
|
||||
<source>Unassign</source>
|
||||
@@ -817,14 +773,6 @@
|
||||
<source>Your email address has been verified.</source>
|
||||
<target>Je e-mailadres is geverifieerd.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YDIkAA1" resname="Your password reset request">
|
||||
<source>Your password reset request</source>
|
||||
<target>Verzoek tot wachtwoordherstel</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="zqHSO6v" resname="try again">
|
||||
<source>try again</source>
|
||||
<target>probeer opnieuw</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -113,10 +113,6 @@
|
||||
<source>Please enter an integer.</source>
|
||||
<target>Vul een geldig getal in.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7VWHcE0" resname="Please enter your email">
|
||||
<source>Please enter your email</source>
|
||||
<target>Voer je e-mailadres in</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="9RtYCIm" resname="Please provide a valid phone number.">
|
||||
<source>Please provide a valid phone number.</source>
|
||||
<target>Vul een geldig telefoonnummer in.</target>
|
||||
|
||||
Reference in New Issue
Block a user