05/26 Fix exported PDF form layout broken 3
This commit is contained in:
+49
-71
@@ -396,63 +396,38 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
story.append(HRFlowable(width='100%', thickness=0.75,
|
story.append(HRFlowable(width='100%', thickness=0.75,
|
||||||
color=C_BORDER, spaceAfter=4))
|
color=C_BORDER, spaceAfter=4))
|
||||||
|
|
||||||
# Build one Table row with cells sized by colSpan.
|
# Build a fixed 12-column table for this grid row.
|
||||||
# Fields with no value are skipped unless they are text/textarea/label —
|
# Each of the 12 grid columns gets exactly UNIT width.
|
||||||
# those always appear so free-text notes are never suppressed.
|
# Fields occupy their designated columns via SPAN directives —
|
||||||
# current_col tracks horizontal position (1-based) so gaps between fields
|
# this is the ReportLab equivalent of CSS grid-column.
|
||||||
# are filled with invisible placeholder cells that preserve column alignment.
|
# Unanswered fields are simply left as empty cells; no placeholder
|
||||||
cells = []
|
# bookkeeping is needed because the table always has all 12 columns.
|
||||||
col_widths = []
|
NCOLS = _GRID_COLS # 12
|
||||||
|
cells_12 = [Paragraph('', STYLES['FieldValue']) for _ in range(NCOLS)]
|
||||||
|
spans = [] # SPAN TableStyle directives
|
||||||
has_content = False
|
has_content = False
|
||||||
current_col = 1
|
|
||||||
|
|
||||||
# Returns True if the field should be hidden from the PDF.
|
|
||||||
def _skip(ftype, val):
|
|
||||||
if ftype == 'rating':
|
|
||||||
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 == 'pass_fail':
|
|
||||||
return not val
|
|
||||||
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
|
|
||||||
|
|
||||||
for field in fields_in_row:
|
for field in fields_in_row:
|
||||||
ftype = field.get('type', '')
|
ftype = field.get('type', '')
|
||||||
col = field.get('col', current_col)
|
col = max(1, int(field.get('col', 1))) # 1-indexed
|
||||||
col_span = field.get('colSpan', 1)
|
col_span = max(1, int(field.get('colSpan', 1)))
|
||||||
|
ci = col - 1 # 0-indexed start
|
||||||
|
ci_end = min(ci + col_span - 1, NCOLS - 1) # 0-indexed end
|
||||||
cell_w = UNIT * col_span
|
cell_w = UNIT * col_span
|
||||||
|
|
||||||
# Fill horizontal gap before this field with an invisible spacer
|
if col_span > 1:
|
||||||
if col > current_col:
|
spans.append(('SPAN', (ci, 0), (ci_end, 0)))
|
||||||
gap_w = UNIT * (col - current_col)
|
|
||||||
cells.append(Paragraph('', STYLES['FieldValue']))
|
|
||||||
col_widths.append(gap_w)
|
|
||||||
current_col = col + col_span
|
|
||||||
|
|
||||||
# ── Inline label (free-standing text element) ─────────────────────
|
# ── Inline label (free-standing text element) ─────────────────────
|
||||||
if ftype == 'label':
|
if ftype == 'label':
|
||||||
fs_map = {'small': 8, 'normal': 9, 'large': 11, 'x-large': 13}
|
fs_map = {'small': 8, 'normal': 9, 'large': 11, 'x-large': 13}
|
||||||
fs = fs_map.get(field.get('font_size', 'normal'), 9)
|
fs = fs_map.get(field.get('font_size', 'normal'), 9)
|
||||||
fw = 'Helvetica-Bold' if field.get('font_weight') == 'bold' else 'Helvetica'
|
fw = 'Helvetica-Bold' if field.get('font_weight') == 'bold' else 'Helvetica'
|
||||||
p = Paragraph(
|
cells_12[ci] = Paragraph(
|
||||||
field.get('text_content', ''),
|
field.get('text_content', ''),
|
||||||
ParagraphStyle('li', fontName=fw, fontSize=fs,
|
ParagraphStyle('li', fontName=fw, fontSize=fs,
|
||||||
textColor=C_DARK, leading=fs + 3),
|
textColor=C_DARK, leading=fs + 3),
|
||||||
)
|
)
|
||||||
cells.append(p)
|
|
||||||
col_widths.append(cell_w)
|
|
||||||
has_content = True
|
has_content = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -460,20 +435,31 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
val = form_data.get(fid, '')
|
val = form_data.get(fid, '')
|
||||||
lbl = field.get('label', '')
|
lbl = field.get('label', '')
|
||||||
|
|
||||||
# ── Skip fields with no meaningful value ──────────────────────────
|
# ── Skip fields with no meaningful value — leave cell empty ────────
|
||||||
|
def _skip(ft, v):
|
||||||
|
if ft == 'rating':
|
||||||
|
return not str(v).isdigit() or int(v) == 0
|
||||||
|
if ft == 'image':
|
||||||
|
ip = os.path.join(static_folder, v) if v else ''
|
||||||
|
return not v or not os.path.exists(ip)
|
||||||
|
if ft == 'signature':
|
||||||
|
return not v or not str(v).startswith('data:')
|
||||||
|
if ft == 'checkbox':
|
||||||
|
return v not in ('yes', 'true')
|
||||||
|
if ft == 'pass_fail':
|
||||||
|
return not v
|
||||||
|
if ft in ('checkbox_group', 'table'):
|
||||||
|
return not v or not isinstance(v, list) or len(v) == 0
|
||||||
|
return not v
|
||||||
|
|
||||||
if _skip(ftype, val):
|
if _skip(ftype, val):
|
||||||
# Keep a placeholder so subsequent fields stay in column position
|
continue # cell stays empty; SPAN ensures correct column width
|
||||||
cells.append(Paragraph('', STYLES['FieldValue']))
|
|
||||||
col_widths.append(cell_w)
|
|
||||||
continue
|
|
||||||
|
|
||||||
lbl_p = Paragraph(lbl, STYLES['FieldLabel'])
|
lbl_p = Paragraph(lbl, STYLES['FieldLabel'])
|
||||||
|
|
||||||
# ── Value content ────────────────────────────────────────────────
|
# ── Value content ────────────────────────────────────────────────
|
||||||
if ftype == 'rating':
|
if ftype == 'rating':
|
||||||
score_int = int(val)
|
score_int = int(val)
|
||||||
# Use ★ for both filled and unfilled — ☆ is unsupported in
|
|
||||||
# Helvetica and renders as a solid box. Grey colour for unfilled.
|
|
||||||
filled_stars = '<font color="#f59e0b">' + ('★' * score_int) + '</font>'
|
filled_stars = '<font color="#f59e0b">' + ('★' * score_int) + '</font>'
|
||||||
empty_stars = ('<font color="#d1d5db">' + ('★' * (5 - score_int)) + '</font>'
|
empty_stars = ('<font color="#d1d5db">' + ('★' * (5 - score_int)) + '</font>'
|
||||||
if score_int < 5 else '')
|
if score_int < 5 else '')
|
||||||
@@ -482,13 +468,8 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
'rv', fontName='Helvetica', fontSize=9, leading=12))
|
'rv', fontName='Helvetica', fontSize=9, leading=12))
|
||||||
|
|
||||||
elif ftype == 'image':
|
elif ftype == 'image':
|
||||||
# File existence already verified in the skip gate above
|
|
||||||
img_path = os.path.join(static_folder, val)
|
img_path = os.path.join(static_folder, val)
|
||||||
try:
|
try:
|
||||||
# Attempt to compress the image before embedding.
|
|
||||||
# _compress_image() resizes to _IMG_MAX_PX and re-encodes
|
|
||||||
# as JPEG at _IMG_JPEG_QUALITY — typically 85–95% smaller
|
|
||||||
# than the original phone photo.
|
|
||||||
compressed = _compress_image(img_path)
|
compressed = _compress_image(img_path)
|
||||||
img_src = compressed if compressed is not None else img_path
|
img_src = compressed if compressed is not None else img_path
|
||||||
val_p = RLImage(img_src,
|
val_p = RLImage(img_src,
|
||||||
@@ -496,10 +477,7 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
height=1.0 * inch,
|
height=1.0 * inch,
|
||||||
kind='proportional')
|
kind='proportional')
|
||||||
except Exception:
|
except Exception:
|
||||||
# Could not load image — insert placeholder to preserve column position
|
continue # leave cell empty
|
||||||
cells.append(Paragraph('', STYLES['FieldValue']))
|
|
||||||
col_widths.append(cell_w)
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif ftype == 'signature':
|
elif ftype == 'signature':
|
||||||
val_p = Paragraph('[Signature captured]', STYLES['FieldValue'])
|
val_p = Paragraph('[Signature captured]', STYLES['FieldValue'])
|
||||||
@@ -509,7 +487,7 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
|
|
||||||
elif ftype == 'pass_fail':
|
elif ftype == 'pass_fail':
|
||||||
is_pass = str(val).lower() in ('pass', 'yes', 'ok', 'good', 'acceptable', 'compliant')
|
is_pass = str(val).lower() in ('pass', 'yes', 'ok', 'good', 'acceptable', 'compliant')
|
||||||
colour = C_GREEN if is_pass else C_RED
|
colour = C_GREEN if is_pass else C_RED
|
||||||
val_p = Paragraph(
|
val_p = Paragraph(
|
||||||
f'<font color="{colour.hexval()}">{str(val)}</font>',
|
f'<font color="{colour.hexval()}">{str(val)}</font>',
|
||||||
STYLES['FieldValue'],
|
STYLES['FieldValue'],
|
||||||
@@ -535,8 +513,7 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# text, textarea, number, date, email, radio, select
|
disp = str(val) if val else ''
|
||||||
disp = str(val) if val else ''
|
|
||||||
val_p = Paragraph(disp, STYLES['FieldValue'])
|
val_p = Paragraph(disp, STYLES['FieldValue'])
|
||||||
|
|
||||||
# ── Wrap into label-over-value form box ──────────────────────────
|
# ── Wrap into label-over-value form box ──────────────────────────
|
||||||
@@ -553,22 +530,23 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
('BACKGROUND', (0, 1), (0, 1), colors.HexColor('#f8fafc')),
|
('BACKGROUND', (0, 1), (0, 1), colors.HexColor('#f8fafc')),
|
||||||
]))
|
]))
|
||||||
|
|
||||||
cells.append(box)
|
cells_12[ci] = box
|
||||||
col_widths.append(cell_w)
|
has_content = True
|
||||||
has_content = True
|
|
||||||
|
|
||||||
# Skip the entire row if no field had meaningful content
|
# Skip the entire row if no field had meaningful content
|
||||||
if not has_content:
|
if not has_content:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
row_tbl = Table([cells], colWidths=col_widths, hAlign='LEFT')
|
row_tbl = Table([cells_12], colWidths=[UNIT] * NCOLS, hAlign='LEFT')
|
||||||
row_tbl.setStyle(TableStyle([
|
row_tbl.setStyle(TableStyle(
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
[
|
||||||
('LEFTPADDING', (0, 0), (-1, -1), 2),
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||||
('RIGHTPADDING', (0, 0), (-1, -1), 2),
|
('LEFTPADDING', (0, 0), (-1, -1), 2),
|
||||||
('TOPPADDING', (0, 0), (-1, -1), 2),
|
('RIGHTPADDING', (0, 0), (-1, -1), 2),
|
||||||
('BOTTOMPADDING',(0, 0), (-1, -1), 2),
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
||||||
]))
|
('BOTTOMPADDING',(0, 0), (-1, -1), 2),
|
||||||
|
] + spans
|
||||||
|
))
|
||||||
story.append(row_tbl)
|
story.append(row_tbl)
|
||||||
|
|
||||||
return story
|
return story
|
||||||
|
|||||||
Reference in New Issue
Block a user