Jul 16 - Fill the gaps between Single-tenant mode and Multi-tenant mode - MT3
This commit is contained in:
+31
-1
@@ -319,4 +319,34 @@ class SetPasswordForm(FlaskForm):
|
||||
def validate_username(self, field):
|
||||
existing = User.query.filter_by(username=field.data.strip()).first()
|
||||
if existing:
|
||||
raise ValidationError('This username is already taken. Please choose another.')
|
||||
raise ValidationError('This username is already taken. Please choose another.')
|
||||
|
||||
# ── Public QR issue report (phase42) ─────────────────────────────────────────
|
||||
|
||||
class PublicIssueReportForm(FlaskForm):
|
||||
"""Login-free issue report submitted from a facility's or area's public QR page.
|
||||
|
||||
Ported from the single-tenant tree, with MT's occupant-chosen `severity`
|
||||
field retained (the ST original always filed at 'medium').
|
||||
|
||||
`website` is a honeypot: real users never see it (hidden via CSS); bots
|
||||
that fill every field trip it and the submission is silently rejected.
|
||||
"""
|
||||
area_label = StringField('Where in the building?',
|
||||
validators=[Optional(), Length(max=120)])
|
||||
description = TextAreaField('Describe the problem',
|
||||
validators=[DataRequired(), Length(min=5, max=2000)])
|
||||
severity = SelectField('How urgent is it?', choices=[
|
||||
('low', 'Minor — can wait'),
|
||||
('medium', 'Normal'),
|
||||
('high', 'Urgent — needs attention today'),
|
||||
], default='medium', validators=[Optional()])
|
||||
reporter_name = StringField('Your name (optional)',
|
||||
validators=[Optional(), Length(max=100)])
|
||||
reporter_contact = StringField('Email or phone (optional)',
|
||||
validators=[Optional(), Length(max=120)])
|
||||
photos = MultipleFileField('Add photos (optional, up to 5)',
|
||||
validators=[Optional(),
|
||||
FileAllowed(['jpg', 'jpeg', 'png', 'gif'],
|
||||
'Images only (jpg, png, gif).')])
|
||||
website = StringField('Website') # honeypot — must stay empty
|
||||
|
||||
+93
-1
@@ -1589,4 +1589,96 @@ def generate_facility_summary_pdf(facility, days, start, now,
|
||||
))
|
||||
doc.build(story)
|
||||
return buf.getvalue()
|
||||
return buf.getvalue()
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# ── QR code sheet (phase42) ───────────────────────────────────────────────────
|
||||
|
||||
def generate_qr_codes_pdf(items, filter_summary: str = '') -> bytes:
|
||||
"""Return a PDF byte-string laying out selected QR codes in a grid.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
items : list of dicts, each:
|
||||
{
|
||||
'title': str, # main label (facility or area name)
|
||||
'subtitle': str | None, # e.g. contract name, or parent facility
|
||||
'caption': str | None, # small line under the QR
|
||||
'png': bytes, # QR code PNG image bytes
|
||||
}
|
||||
filter_summary : human-readable string describing the selection (optional)
|
||||
"""
|
||||
buf = io.BytesIO()
|
||||
generated_at = datetime.now().strftime('%B %d, %Y %I:%M %p ET')
|
||||
report_title = 'QR Codes'
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
buf,
|
||||
pagesize=letter,
|
||||
leftMargin=0.65 * inch,
|
||||
rightMargin=0.65 * inch,
|
||||
topMargin=1.35 * inch,
|
||||
bottomMargin=0.75 * inch,
|
||||
title=report_title,
|
||||
author='Janitorial QC System',
|
||||
)
|
||||
|
||||
def _page_cb(canvas, doc):
|
||||
_on_page(canvas, doc, report_title, generated_at)
|
||||
|
||||
story = []
|
||||
if filter_summary:
|
||||
story.append(Paragraph(f'Filters: {filter_summary}', STYLES['ReportSub']))
|
||||
story.append(Paragraph(
|
||||
f'Total codes: {len(items)}', STYLES['ReportSub']))
|
||||
story.append(Spacer(1, 10))
|
||||
|
||||
if not items:
|
||||
story.append(Paragraph('No QR codes selected.', STYLES['FieldValue']))
|
||||
doc.build(story, onFirstPage=_page_cb, onLaterPages=_page_cb)
|
||||
return buf.getvalue()
|
||||
|
||||
COLS = 3
|
||||
pw = letter[0] - 1.3 * inch # usable width
|
||||
cell_w = pw / COLS
|
||||
qr_size = 1.7 * inch
|
||||
|
||||
title_style = ParagraphStyle('QRT', fontName='Helvetica-Bold', fontSize=9,
|
||||
alignment=TA_CENTER, leading=11, textColor=C_DARK)
|
||||
sub_style = ParagraphStyle('QRS', fontName='Helvetica', fontSize=7.5,
|
||||
alignment=TA_CENTER, leading=9, textColor=C_SLATE)
|
||||
cap_style = ParagraphStyle('QRC', fontName='Helvetica', fontSize=6.5,
|
||||
alignment=TA_CENTER, leading=8, textColor=C_SLATE)
|
||||
|
||||
def _cell(item):
|
||||
flow = [Paragraph(item.get('title') or '', title_style)]
|
||||
if item.get('subtitle'):
|
||||
flow.append(Paragraph(item['subtitle'], sub_style))
|
||||
flow.append(Spacer(1, 4))
|
||||
flow.append(RLImage(io.BytesIO(item['png']), width=qr_size, height=qr_size))
|
||||
if item.get('caption'):
|
||||
flow.append(Spacer(1, 3))
|
||||
flow.append(Paragraph(item['caption'], cap_style))
|
||||
return flow
|
||||
|
||||
rows = []
|
||||
for i in range(0, len(items), COLS):
|
||||
chunk = items[i:i + COLS]
|
||||
row = [_cell(it) for it in chunk]
|
||||
while len(row) < COLS:
|
||||
row.append('') # filler cell to keep the grid rectangular
|
||||
rows.append(row)
|
||||
|
||||
tbl = Table(rows, colWidths=[cell_w] * COLS)
|
||||
tbl.setStyle(TableStyle([
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 10),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 16),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
||||
]))
|
||||
story.append(tbl)
|
||||
|
||||
doc.build(story, onFirstPage=_page_cb, onLaterPages=_page_cb)
|
||||
return buf.getvalue()
|
||||
|
||||
Reference in New Issue
Block a user