Jul 17 - Update Facility/Area QR code report page with anti-duplication reports

This commit is contained in:
2026-07-17 10:24:59 -04:00
parent 4900ddc2cc
commit 08b9f088a8
4 changed files with 121 additions and 30 deletions
+50 -7
View File
@@ -62,6 +62,31 @@ def _save_report_photos(file_list):
return photo_path, extra_paths
#: Window within which an identical public report is treated as a duplicate.
DUPLICATE_REPORT_WINDOW_SECONDS = 60
def _recent_duplicate_report(facility_id, area_id, description):
"""Return True if an identical public report was just filed.
Belt-and-suspenders against duplicate submissions (double-taps, JS-disabled
clients, retries): if a public issue (reported_by IS NULL) with the same
facility/area and identical description was created within the last
DUPLICATE_REPORT_WINDOW_SECONDS, treat this one as a duplicate and skip it.
"""
cutoff = now_eastern() - timedelta(seconds=DUPLICATE_REPORT_WINDOW_SECONDS)
q = Issue.query.filter(
Issue.reported_by.is_(None),
Issue.reported_at >= cutoff,
Issue.description == description,
)
if area_id is not None:
q = q.filter(Issue.area_id == area_id)
else:
q = q.filter(Issue.facility_id == facility_id, Issue.area_id.is_(None))
return db.session.query(q.exists()).scalar()
def _facility_by_token_or_404(token: str) -> Facility:
"""Resolve an ACTIVE facility from its public token, else 404."""
if not token:
@@ -344,10 +369,6 @@ def report_problem(token):
return render_template('public/facility.html',
form=form, token=token, **summary), 400
# Save up to 5 optional photos through the shared, magic-byte-validated
# saver. First → photo_path, the rest → mobile_photo_paths.
photo_path, extra_photos = _save_report_photos(form.photos.data)
# Fold optional reporter identity + location into the description; the
# public reporter is not a User, so reported_by stays NULL.
parts = ['[Reported via facility QR code]']
@@ -360,6 +381,19 @@ def report_problem(token):
parts.append(form.description.data.strip())
description = '\n'.join(parts)
# Server-side idempotency: silently accept an identical repeat as success
# without creating a second issue (or saving its photos).
if _recent_duplicate_report(facility.id, None, description):
logger.info('PUBLIC REPORT | duplicate suppressed | facility_id=%s | ip=%s',
facility.id, request.remote_addr)
flash('Thank you — your report has been received and the team has been notified.',
'success')
return redirect(url_for('public.facility_summary', token=token))
# Save up to 5 optional photos through the shared, magic-byte-validated
# saver. First → photo_path, the rest → mobile_photo_paths.
photo_path, extra_photos = _save_report_photos(form.photos.data)
issue = Issue(
facility_id = facility.id,
area_id = None,
@@ -416,9 +450,6 @@ def area_report_problem(token):
return render_template('public/area.html',
form=form, token=token, **summary), 400
# Save up to 5 optional photos (first → photo_path, rest → mobile_photo_paths).
photo_path, extra_photos = _save_report_photos(form.photos.data)
# The area is known from the QR token, so we set area_id directly and note
# the source. A public reporter is not a User, so reported_by stays NULL.
parts = [f'[Reported via area QR code — {area.name}]']
@@ -431,6 +462,18 @@ def area_report_problem(token):
parts.append(form.description.data.strip())
description = '\n'.join(parts)
# Server-side idempotency: silently accept an identical repeat as success
# without creating a second issue (or saving its photos).
if _recent_duplicate_report(facility.id, area.id, description):
logger.info('PUBLIC REPORT | duplicate suppressed | area_id=%s | facility_id=%s | ip=%s',
area.id, facility.id, request.remote_addr)
flash('Thank you — your report has been received and the team has been notified.',
'success')
return redirect(url_for('public.area_summary', token=token))
# Save up to 5 optional photos (first → photo_path, rest → mobile_photo_paths).
photo_path, extra_photos = _save_report_photos(form.photos.data)
issue = Issue(
facility_id = facility.id,
area_id = area.id,
+35 -11
View File
@@ -124,7 +124,7 @@
Let the cleaning team know.
</p>
<form method="POST"
<form method="POST" id="reportForm"
action="{{ url_for('public.area_report_problem', token=token) }}"
enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
@@ -171,7 +171,7 @@
{% endfor %}
</div>
<button type="submit" class="btn btn-primary w-100">
<button type="submit" class="btn btn-primary w-100" id="submitBtn">
<i class="bi bi-send"></i> Submit Report
</button>
</form>
@@ -184,17 +184,41 @@
<script>
(function () {
'use strict';
// Cap photo selection at 5.
var input = document.getElementById('reportPhotos');
var msg = document.getElementById('photoLimitMsg');
if (!input) { return; }
input.addEventListener('change', function () {
if (input.files && input.files.length > 5) {
if (msg) { msg.style.display = 'block'; }
input.value = ''; // clear an over-limit selection so they re-pick
} else if (msg) {
msg.style.display = 'none';
}
});
if (input) {
input.addEventListener('change', function () {
if (input.files && input.files.length > 5) {
if (msg) { msg.style.display = 'block'; }
input.value = ''; // clear an over-limit selection so they re-pick
} else if (msg) {
msg.style.display = 'none';
}
});
}
// Prevent duplicate reports: disable the button on first submit so a slow
// network can't be double-tapped into multiple identical reports.
var form = document.getElementById('reportForm');
var btn = document.getElementById('submitBtn');
if (form) {
form.addEventListener('submit', function (e) {
if (form.dataset.submitting === '1') { e.preventDefault(); return; }
form.dataset.submitting = '1';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Submitting…';
}
});
}
// After a successful submit we redirect back here with a flash message;
// jump to the top so the confirmation is seen and the form isn't re-tapped.
if (document.querySelector('.alert')) {
window.scrollTo(0, 0);
}
}());
</script>
</body>
+35 -11
View File
@@ -123,7 +123,7 @@
Notice something that needs attention? Let the cleaning team know.
</p>
<form method="POST"
<form method="POST" id="reportForm"
action="{{ url_for('public.report_problem', token=token) }}"
enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
@@ -170,7 +170,7 @@
{% endfor %}
</div>
<button type="submit" class="btn btn-primary w-100">
<button type="submit" class="btn btn-primary w-100" id="submitBtn">
<i class="bi bi-send"></i> Submit Report
</button>
</form>
@@ -183,17 +183,41 @@
<script>
(function () {
'use strict';
// Cap photo selection at 5.
var input = document.getElementById('reportPhotos');
var msg = document.getElementById('photoLimitMsg');
if (!input) { return; }
input.addEventListener('change', function () {
if (input.files && input.files.length > 5) {
if (msg) { msg.style.display = 'block'; }
input.value = ''; // clear an over-limit selection so they re-pick
} else if (msg) {
msg.style.display = 'none';
}
});
if (input) {
input.addEventListener('change', function () {
if (input.files && input.files.length > 5) {
if (msg) { msg.style.display = 'block'; }
input.value = ''; // clear an over-limit selection so they re-pick
} else if (msg) {
msg.style.display = 'none';
}
});
}
// Prevent duplicate reports: disable the button on first submit so a slow
// network can't be double-tapped into multiple identical reports.
var form = document.getElementById('reportForm');
var btn = document.getElementById('submitBtn');
if (form) {
form.addEventListener('submit', function (e) {
if (form.dataset.submitting === '1') { e.preventDefault(); return; }
form.dataset.submitting = '1';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Submitting…';
}
});
}
// After a successful submit we redirect back here with a flash message;
// jump to the top so the confirmation is seen and the form isn't re-tapped.
if (document.querySelector('.alert')) {
window.scrollTo(0, 0);
}
}());
</script>
</body>