Jul 10 - Update facility's area with QR code

This commit is contained in:
2026-07-10 14:05:54 -04:00
parent 008dd94962
commit 512a80837a
8 changed files with 635 additions and 6 deletions
+190
View File
@@ -170,6 +170,119 @@ def _build_summary(facility: Facility) -> dict:
}
def _area_by_token_or_404(token):
"""Resolve an area (and its ACTIVE facility) from the area's public token."""
if not token:
abort(404)
area = Area.query.filter_by(public_token=token).first()
if area is None:
abort(404)
facility = db.session.get(Facility, area.facility_id)
if facility is None or not facility.active:
abort(404)
return area, facility
def _build_area_summary(area, facility) -> dict:
"""Assemble the occupant-facing summary for a single area.
Occupant-safe (rule 74): same aggregate-only shape as the facility page,
but every metric is scoped to this area's inspections/issues.
"""
aid = area.id
now = now_eastern()
cutoff_90 = now - timedelta(days=90)
cutoff_30 = now - timedelta(days=30)
cutoff_60 = now - timedelta(days=60)
last_insp = (
Inspection.query
.filter(Inspection.area_id == aid,
Inspection.status == 'completed',
Inspection.overall_score.isnot(None))
.order_by(Inspection.inspection_date.desc())
.first()
)
def _avg_between(start, end=None):
q = (db.session.query(func.avg(Inspection.overall_score))
.filter(Inspection.area_id == aid,
Inspection.status == 'completed',
Inspection.overall_score.isnot(None),
Inspection.inspection_date >= start))
if end is not None:
q = q.filter(Inspection.inspection_date < end)
return q.scalar()
avg_90 = _avg_between(cutoff_90)
if avg_90 is None:
avg_90 = (
db.session.query(func.avg(Inspection.overall_score))
.filter(Inspection.area_id == aid,
Inspection.status == 'completed',
Inspection.overall_score.isnot(None))
.scalar()
)
avg_score = round(float(avg_90), 1) if avg_90 is not None else None
inspections_90 = (
Inspection.query
.filter(Inspection.area_id == aid,
Inspection.status == 'completed',
Inspection.inspection_date >= cutoff_90)
.count()
)
avg_cur = _avg_between(cutoff_30)
avg_prior = _avg_between(cutoff_60, cutoff_30)
if avg_cur is not None and avg_prior is not None:
trend_delta = round(float(avg_cur) - float(avg_prior), 1)
else:
trend_delta = None
recent = (
Inspection.query
.filter(Inspection.area_id == aid,
Inspection.status == 'completed')
.order_by(Inspection.inspection_date.desc())
.limit(5)
.all()
)
recent_dates = [i.inspection_date for i in recent]
open_issue_count = (
Issue.query
.filter(Issue.status.in_(['open', 'in_progress']),
Issue.area_id == aid)
.count()
)
resolved_90 = (
Issue.query
.filter(Issue.status == 'resolved',
Issue.resolved_at.isnot(None),
Issue.resolved_at >= cutoff_90,
Issue.area_id == aid)
.count()
)
label, colour = _rating_label(avg_score)
return {
'facility': facility,
'area': area,
'avg_score': avg_score,
'rating_label': label,
'rating_colour': colour,
'inspections_90': inspections_90,
'open_issue_count': open_issue_count,
'resolved_90': resolved_90,
'trend_delta': trend_delta,
'last_inspected': last_insp.inspection_date if last_insp else None,
'recent_dates': recent_dates,
}
@bp.route('/<token>', methods=['GET'])
def facility_summary(token):
facility = _facility_by_token_or_404(token)
@@ -179,6 +292,15 @@ def facility_summary(token):
form=form, token=token, **summary)
@bp.route('/area/<token>', methods=['GET'])
def area_summary(token):
area, facility = _area_by_token_or_404(token)
summary = _build_area_summary(area, facility)
form = PublicIssueReportForm()
return render_template('public/area.html',
form=form, token=token, **summary)
@bp.route('/<token>/report', methods=['POST'])
@limiter.limit('5 per hour; 20 per day')
def report_problem(token):
@@ -248,3 +370,71 @@ def report_problem(token):
flash('Thank you — your report has been received and the team has been notified.',
'success')
return redirect(url_for('public.facility_summary', token=token))
@bp.route('/area/<token>/report', methods=['POST'])
@limiter.limit('5 per hour; 20 per day')
def area_report_problem(token):
area, facility = _area_by_token_or_404(token)
form = PublicIssueReportForm()
# Honeypot: silently accept-and-drop obvious bot submissions.
if form.website.data:
logger.info('PUBLIC REPORT | honeypot tripped | area_id=%s | ip=%s',
area.id, request.remote_addr)
flash('Thank you — your report has been received.', 'success')
return redirect(url_for('public.area_summary', token=token))
if not form.validate_on_submit():
summary = _build_area_summary(area, facility)
return render_template('public/area.html',
form=form, token=token, **summary), 400
from app.routes.inspections import _save_photo
photo_path = _save_photo(form.photo.data, subfolder='issue_photos')
# 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}]']
if form.area_label.data:
parts.append(f'Location: {form.area_label.data.strip()}')
reporter_bits = [b for b in (form.reporter_name.data, form.reporter_contact.data) if b]
if reporter_bits:
parts.append('Reporter: ' + ''.join(b.strip() for b in reporter_bits))
parts.append('')
parts.append(form.description.data.strip())
description = '\n'.join(parts)
issue = Issue(
facility_id = facility.id,
area_id = area.id,
severity = 'medium',
description = description,
photo_path = photo_path,
status = 'open',
reported_at = now_eastern(),
reported_by = None,
)
db.session.add(issue)
db.session.commit()
logger.info('PUBLIC REPORT | issue_id=%s | area_id=%s | facility_id=%s | ip=%s | photo=%s',
issue.id, area.id, facility.id, request.remote_addr, bool(photo_path))
notify_by_matrix(
event_type = 'issue_created',
title = f'New Issue #{issue.id} at {facility.name}{area.name} (QR report)',
body = (
f'A problem was reported in {area.name} at {facility.name} via the area '
f'QR code. Description: {form.description.data.strip()[:120]}'
f'{"" if len(form.description.data.strip()) > 120 else ""}'
),
link = url_for('issues.view', issue_id=issue.id),
issue_id = issue.id,
facility_id = facility.id,
)
db.session.commit()
flash('Thank you — your report has been received and the team has been notified.',
'success')
return redirect(url_for('public.area_summary', token=token))