From 4a6a3ba4fb734f7878f87d056303adbb166633c5 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Sun, 1 Mar 2026 15:38:31 -0500 Subject: [PATCH] Feb 02 2026: change export file format 4 --- app/routes/inspections.py | 11 +++++ app/utils/pdf_export.py | 93 ++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/app/routes/inspections.py b/app/routes/inspections.py index 057727c..48eaddf 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -448,6 +448,17 @@ def export_pdf(inspection_id): static_folder = os.path.join(current_app.root_path, 'static') + # Log image field values to help diagnose missing-photo issues in PDF export + for field in form_fields: + if field.get('type') == 'image': + fid = str(field.get('id', '')) + val = form_data.get(fid, '') + resolved = os.path.join(static_folder, val) if val else '' + current_app.logger.info( + 'PDF export image field | fid=%s | val=%r | exists=%s | resolved=%r', + fid, val, os.path.exists(resolved) if resolved else False, resolved + ) + pdf_bytes = generate_inspection_pdf( inspection = inspection, form_fields = form_fields, diff --git a/app/utils/pdf_export.py b/app/utils/pdf_export.py index c9900c9..9d88712 100644 --- a/app/utils/pdf_export.py +++ b/app/utils/pdf_export.py @@ -234,8 +234,13 @@ def _score_banner(inspection): # ── Form fields section ─────────────────────────────────────────────────────── def _star_string(score, max_stars=5): + """Return a star string using only the filled ★ glyph. + Hollow ☆ is not supported by Helvetica and renders as a box. + Unselected stars are rendered as grey ★ via ReportLab XML markup. + Returns a plain string for canvas use; use _star_markup() for Paragraph. + """ filled = min(int(score), max_stars) - return '★' * filled + '☆' * (max_stars - filled) + return '★' * filled + '★' * (max_stars - filled) _SKIP_TYPES = {'button_submit', 'button_print', 'button_email'} @@ -285,7 +290,7 @@ def _form_fields_section(form_fields, form_data, static_folder): # A row is visible if it contains a text/textarea/label field, OR any other # field type that has a non-empty value. This is used to suppress section # banners whose every subordinate row is empty. - _ALWAYS_SHOW_PRE = {'text', 'textarea', 'label'} + _ALWAYS_SHOW_PRE = {'label'} def _row_has_content(fields_in_row): for f in fields_in_row: @@ -295,12 +300,13 @@ def _form_fields_section(form_fields, form_data, static_folder): if ft in _ALWAYS_SHOW_PRE: return True if ft == 'rating' and str(v).isdigit() and int(v) > 0: return True - if ft == 'checkbox' and v == 'yes': return True + if ft == 'checkbox' and v in ('yes','true'): return True if ft == 'checkbox_group'and isinstance(v, list) and v: return True - if ft == 'image' and v: return True + if ft == 'image' and v and os.path.exists(os.path.join(static_folder, v)): return True if ft == 'signature' and v and str(v).startswith('data:'): return True if ft == 'table' and isinstance(v, list) and v: return True if ft in ('number','date','email','radio','select') and v: return True + if ft in ('text','textarea') and v: return True return False # Build a set of section-trigger row numbers that have visible content somewhere @@ -344,9 +350,6 @@ def _form_fields_section(form_fields, form_data, static_folder): cells = [] col_widths = [] - # Types that are always shown regardless of value - _ALWAYS_SHOW = {'text', 'textarea', 'label'} - for field in fields_in_row: ftype = field.get('type', '') col_span = field.get('colSpan', 1) @@ -370,57 +373,55 @@ def _form_fields_section(form_fields, form_data, static_folder): val = form_data.get(fid, '') lbl = field.get('label', '') - # ── Skip fields with no value unless type is always-shown ───────── - if ftype not in _ALWAYS_SHOW: + # ── Skip fields with no meaningful value ────────────────────────── + # Returns True if the field should be hidden from the PDF. + def _skip(ftype, val): if ftype == 'rating': - if not str(val).isdigit() or int(val) == 0: - continue - elif ftype == 'checkbox': - if val != 'yes': - continue - elif ftype == 'checkbox_group': - if not val or not isinstance(val, list) or len(val) == 0: - continue - elif ftype == 'image': - if not val: - continue - elif ftype == 'signature': - if not val or not str(val).startswith('data:'): - continue - elif ftype == 'table': - if not val or not isinstance(val, list) or len(val) == 0: - continue - elif ftype in ('number', 'date', 'email', 'radio', 'select'): - if not val: - continue + return not str(val).isdigit() or int(val) == 0 + if ftype == 'image': + img_path = os.path.join(static_folder, val) if val else '' + return not val or not os.path.exists(img_path) + if ftype == 'signature': + return not val or not str(val).startswith('data:') + if ftype == 'checkbox': + # stored as 'true'/'false' by _collect_form_responses + return val not in ('yes', 'true') + if ftype == 'checkbox_group': + return not val or not isinstance(val, list) or len(val) == 0 + if ftype == 'table': + return not val or not isinstance(val, list) or len(val) == 0 + # All remaining input types (text, textarea, number, date, + # email, radio, select, and any future types) + return not val + + if _skip(ftype, val): + continue lbl_p = Paragraph(lbl, STYLES['FieldLabel']) # ── Value content ──────────────────────────────────────────────── if ftype == 'rating': score_int = int(val) - stars = (f'{"★" * score_int}' - f'{"☆" * (5 - score_int)}' - f' {score_int}/5') + # Use ★ for both filled and unfilled — ☆ is unsupported in + # Helvetica and renders as a solid box. Grey colour for unfilled. + filled_stars = '' + ('★' * score_int) + '' + empty_stars = ('' + ('★' * (5 - score_int)) + '' + if score_int < 5 else '') + stars = filled_stars + empty_stars + f' {score_int}/5' val_p = Paragraph(stars, ParagraphStyle( 'rv', fontName='Helvetica', fontSize=9, leading=12)) elif ftype == 'image': + # File existence already verified in the skip gate above img_path = os.path.join(static_folder, val) - if os.path.exists(img_path): - try: - val_p = RLImage(img_path, - width=min(cell_w - 12, 1.4 * inch), - height=1.0 * inch, - kind='proportional') - except Exception: - val_p = Paragraph( - 'Photo error', - STYLES['FieldValue']) - else: - val_p = Paragraph( - 'File not found', - STYLES['FieldValue']) + try: + val_p = RLImage(img_path, + width=min(cell_w - 12, 1.4 * inch), + height=1.0 * inch, + kind='proportional') + except Exception: + # Could not load image — skip this cell entirely + continue elif ftype == 'signature': val_p = Paragraph('[Signature captured]', STYLES['FieldValue'])