fix(classifieds,admin): blokada duplikatów przez double/triple-click
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
B2B ogłoszenia mogły zostać stworzone 3x (user 81 Bormax 14.04.2026 w ciągu 2 sekund) — brak dedup window server-side i disable submit button. Rozszerzam zabezpieczenie także na announcements i board meeting form. - classifieds POST /nowe: odrzuć duplikat z ostatnich 60s (ten sam author+company+title) → redirect do istniejącego z flash info - classifieds new.html: disable submitBtn + "Wysyłanie..." po walidacji; ponowne kliknięcie blokowane event.preventDefault - announcements_form.html + board/meeting_form.html: jednolity handler disable wszystkich button[type="submit"] po pierwszym submit Forum topic/reply już miały analogiczne zabezpieczenie (bez zmian). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
958b967df2
commit
6c248b4773
@ -116,6 +116,18 @@ def new():
|
|||||||
).first():
|
).first():
|
||||||
form_company_id = current_user.company_id
|
form_company_id = current_user.company_id
|
||||||
|
|
||||||
|
# Deduplikacja double/triple-click: jeśli ten sam autor+firma+tytuł
|
||||||
|
# wpadły w ostatnich 60 sekundach — traktuj jako powtórkę przesyłu formularza.
|
||||||
|
recent_duplicate = db.query(Classified).filter(
|
||||||
|
Classified.author_id == current_user.id,
|
||||||
|
Classified.company_id == form_company_id,
|
||||||
|
Classified.title == title,
|
||||||
|
Classified.created_at >= datetime.now() - timedelta(seconds=60),
|
||||||
|
).order_by(Classified.created_at.desc()).first()
|
||||||
|
if recent_duplicate:
|
||||||
|
flash('To ogłoszenie właśnie dodano — wyświetlamy istniejące.', 'info')
|
||||||
|
return redirect(url_for('.classifieds_view', classified_id=recent_duplicate.id))
|
||||||
|
|
||||||
classified = Classified(
|
classified = Classified(
|
||||||
author_id=current_user.id,
|
author_id=current_user.id,
|
||||||
company_id=form_company_id,
|
company_id=form_company_id,
|
||||||
|
|||||||
@ -169,7 +169,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<form method="POST">
|
<form method="POST" id="announcementForm">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
@ -301,6 +301,24 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
// Anty double/triple-click — blokada wielokrotnego submitu
|
||||||
|
(function() {
|
||||||
|
const form = document.getElementById('announcementForm');
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const btns = form.querySelectorAll('button[type="submit"]');
|
||||||
|
if (Array.from(btns).some(function(b) { return b.disabled; })) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btns.forEach(function(b) {
|
||||||
|
b.dataset.originalText = b.textContent.trim();
|
||||||
|
b.disabled = true;
|
||||||
|
b.textContent = 'Wysyłanie...';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Character counter for excerpt
|
// Character counter for excerpt
|
||||||
const excerptTextarea = document.getElementById('excerpt');
|
const excerptTextarea = document.getElementById('excerpt');
|
||||||
const excerptCounter = document.getElementById('excerpt-counter');
|
const excerptCounter = document.getElementById('excerpt-counter');
|
||||||
|
|||||||
@ -723,6 +723,24 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
// Anty double/triple-click — blokada wielokrotnego submitu
|
||||||
|
(function() {
|
||||||
|
const form = document.getElementById('meetingForm');
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const btns = form.querySelectorAll('button[type="submit"]');
|
||||||
|
if (Array.from(btns).some(function(b) { return b.disabled; })) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btns.forEach(function(b) {
|
||||||
|
b.dataset.originalText = b.innerHTML;
|
||||||
|
b.disabled = true;
|
||||||
|
b.textContent = 'Wysyłanie...';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Tab switching
|
// Tab switching
|
||||||
document.querySelectorAll('.form-tab').forEach(tab => {
|
document.querySelectorAll('.form-tab').forEach(tab => {
|
||||||
tab.addEventListener('click', function() {
|
tab.addEventListener('click', function() {
|
||||||
|
|||||||
@ -435,7 +435,13 @@ var quill = new Quill('#quill-editor', {
|
|||||||
// their green check immediately on page load.
|
// their green check immediately on page load.
|
||||||
refreshTitle(); refreshCat(); refreshRadios(); refreshDesc();
|
refreshTitle(); refreshCat(); refreshRadios(); refreshDesc();
|
||||||
|
|
||||||
|
var submitBtn = document.getElementById('submitBtn');
|
||||||
document.getElementById('classifiedForm').addEventListener('submit', function(e) {
|
document.getElementById('classifiedForm').addEventListener('submit', function(e) {
|
||||||
|
// Anty double/triple-click — jeśli już wysyłamy, blokuj kolejny submit
|
||||||
|
if (submitBtn && submitBtn.disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var html = quill.root.innerHTML;
|
var html = quill.root.innerHTML;
|
||||||
var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
|
var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
|
||||||
if (empty) {
|
if (empty) {
|
||||||
@ -447,6 +453,10 @@ var quill = new Quill('#quill-editor', {
|
|||||||
}
|
}
|
||||||
qc && qc.classList.remove('field-error');
|
qc && qc.classList.remove('field-error');
|
||||||
document.getElementById('description').value = html;
|
document.getElementById('description').value = html;
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Wysyłanie...';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -565,7 +575,11 @@ var quill = new Quill('#quill-editor', {
|
|||||||
filesMap.forEach(file => formData.append('attachments[]', file));
|
filesMap.forEach(file => formData.append('attachments[]', file));
|
||||||
fetch(form.action, { method: 'POST', body: formData })
|
fetch(form.action, { method: 'POST', body: formData })
|
||||||
.then(resp => { if (resp.redirected) { window.location.href = resp.url; } else { window.location.reload(); } })
|
.then(resp => { if (resp.redirected) { window.location.href = resp.url; } else { window.location.reload(); } })
|
||||||
.catch(() => alert('Błąd wysyłania'));
|
.catch(() => {
|
||||||
|
alert('Błąd wysyłania');
|
||||||
|
var btn = document.getElementById('submitBtn');
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = 'Dodaj ogłoszenie'; }
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatFileSize(bytes) {
|
function formatFileSize(bytes) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user