Feb 02 2026: change export file format 2
This commit is contained in:
+106
-57
@@ -281,14 +281,54 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
||||
|
||||
rows_map[row_num].append(field)
|
||||
|
||||
# ── Pre-compute which rows have at least one visible field ───────────────
|
||||
# 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'}
|
||||
|
||||
def _row_has_content(fields_in_row):
|
||||
for f in fields_in_row:
|
||||
ft = f.get('type', '')
|
||||
fid = str(f.get('id', ''))
|
||||
v = form_data.get(fid, '')
|
||||
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_group'and isinstance(v, list) and v: return True
|
||||
if ft == 'image' and 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
|
||||
return False
|
||||
|
||||
# Build a set of section-trigger row numbers that have visible content somewhere
|
||||
# in their subordinate rows (from their row_num up to the next section's row_num).
|
||||
section_row_nums = sorted(sections.keys())
|
||||
all_row_nums = list(rows_map.keys())
|
||||
|
||||
def _section_has_visible_rows(sec_row_num):
|
||||
idx = section_row_nums.index(sec_row_num)
|
||||
next_sec = section_row_nums[idx + 1] if idx + 1 < len(section_row_nums) else None
|
||||
for rn in all_row_nums:
|
||||
if rn < sec_row_num:
|
||||
continue
|
||||
if next_sec is not None and rn >= next_sec:
|
||||
break
|
||||
if _row_has_content(rows_map[rn]):
|
||||
return True
|
||||
return False
|
||||
|
||||
# ── Render row by row ─────────────────────────────────────────────────────
|
||||
for row_num, fields_in_row in rows_map.items():
|
||||
|
||||
# Emit section banner if one precedes this row
|
||||
# Emit section banner only if its subordinate rows have visible content
|
||||
if row_num in sections:
|
||||
if not _section_has_visible_rows(row_num):
|
||||
continue # skip the banner — all rows beneath it are empty
|
||||
story.append(Spacer(1, 6))
|
||||
sec_label = sections[row_num]
|
||||
# Bold letter prefix (e.g. "A.", "B.") + rest of label
|
||||
story.append(Paragraph(
|
||||
sec_label,
|
||||
ParagraphStyle('SecBanner', fontName='Helvetica-Bold',
|
||||
@@ -298,16 +338,21 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
||||
story.append(HRFlowable(width='100%', thickness=0.75,
|
||||
color=C_BORDER, spaceAfter=4))
|
||||
|
||||
# Build one Table row with cells sized by colSpan
|
||||
# Build one Table row with cells sized by colSpan.
|
||||
# Fields with no value are skipped unless they are text/textarea/label —
|
||||
# those always appear so free-text notes are never suppressed.
|
||||
cells = []
|
||||
col_widths = []
|
||||
has_content = False
|
||||
|
||||
# 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)
|
||||
cell_w = UNIT * col_span
|
||||
|
||||
# ── Inline label (free-standing text element) ─────────────────────
|
||||
if ftype == 'label':
|
||||
fs_map = {'small': 8, 'normal': 9, 'large': 11, 'x-large': 13}
|
||||
fs = fs_map.get(field.get('font_size', 'normal'), 9)
|
||||
@@ -319,87 +364,91 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
||||
)
|
||||
cells.append(p)
|
||||
col_widths.append(cell_w)
|
||||
has_content = True
|
||||
continue
|
||||
|
||||
fid = str(field.get('id', ''))
|
||||
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:
|
||||
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
|
||||
|
||||
lbl_p = Paragraph(lbl, STYLES['FieldLabel'])
|
||||
|
||||
# ── Value content ────────────────────────────────────────────────
|
||||
if ftype == 'rating':
|
||||
score_int = int(val) if str(val).isdigit() else 0
|
||||
if score_int > 0:
|
||||
stars = (f'<font color="#f59e0b">{"★" * score_int}'
|
||||
f'{"☆" * (5 - score_int)}</font>'
|
||||
f' <font color="#64748b">{score_int}/5</font>')
|
||||
else:
|
||||
stars = '<font color="#94a3b8"><i>Not rated</i></font>'
|
||||
score_int = int(val)
|
||||
stars = (f'<font color="#f59e0b">{"★" * score_int}'
|
||||
f'{"☆" * (5 - score_int)}</font>'
|
||||
f' <font color="#64748b">{score_int}/5</font>')
|
||||
val_p = Paragraph(stars, ParagraphStyle(
|
||||
'rv', fontName='Helvetica', fontSize=9, leading=12))
|
||||
|
||||
elif ftype == 'image':
|
||||
if val:
|
||||
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(
|
||||
'<i><font color="#94a3b8">Photo error</font></i>',
|
||||
STYLES['FieldValue'])
|
||||
else:
|
||||
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(
|
||||
'<i><font color="#94a3b8">No photo</font></i>',
|
||||
'<i><font color="#94a3b8">Photo error</font></i>',
|
||||
STYLES['FieldValue'])
|
||||
else:
|
||||
val_p = Paragraph(
|
||||
'<i><font color="#94a3b8">No photo</font></i>',
|
||||
'<i><font color="#94a3b8">File not found</font></i>',
|
||||
STYLES['FieldValue'])
|
||||
|
||||
elif ftype == 'signature':
|
||||
val_p = Paragraph(
|
||||
'[Signature captured]' if (val and str(val).startswith('data:'))
|
||||
else '<i><font color="#94a3b8">No signature</font></i>',
|
||||
STYLES['FieldValue'])
|
||||
val_p = Paragraph('[Signature captured]', STYLES['FieldValue'])
|
||||
|
||||
elif ftype == 'checkbox':
|
||||
val_p = Paragraph('Yes' if val == 'yes' else 'No',
|
||||
STYLES['FieldValue'])
|
||||
val_p = Paragraph('Yes', STYLES['FieldValue'])
|
||||
|
||||
elif ftype == 'checkbox_group':
|
||||
val_p = Paragraph(
|
||||
', '.join(val) if (val and isinstance(val, list))
|
||||
else '<i><font color="#94a3b8">None</font></i>',
|
||||
STYLES['FieldValue'])
|
||||
val_p = Paragraph(', '.join(val), STYLES['FieldValue'])
|
||||
|
||||
elif ftype == 'table':
|
||||
if val and isinstance(val, list) and len(val) > 0:
|
||||
headers = field.get('col_headers',
|
||||
list(val[0].keys()) if val else [])
|
||||
tbl_data = ([headers] +
|
||||
[[r.get(h, '') for h in headers] for r in val])
|
||||
val_p = Table(tbl_data)
|
||||
val_p.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), C_LIGHT),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 7),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.25, C_BORDER),
|
||||
('BOX', (0, 0), (-1, -1), 0.5, C_BORDER),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 2),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
|
||||
]))
|
||||
else:
|
||||
val_p = Paragraph(
|
||||
'<i><font color="#94a3b8">No data</font></i>',
|
||||
STYLES['FieldValue'])
|
||||
headers = field.get('col_headers',
|
||||
list(val[0].keys()) if val else [])
|
||||
tbl_data = ([headers] +
|
||||
[[r.get(h, '') for h in headers] for r in val])
|
||||
val_p = Table(tbl_data)
|
||||
val_p.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), C_LIGHT),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 7),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.25, C_BORDER),
|
||||
('BOX', (0, 0), (-1, -1), 0.5, C_BORDER),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 2),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
|
||||
]))
|
||||
|
||||
else:
|
||||
# text, textarea, number, date, email, radio, select
|
||||
disp = str(val) if val else ''
|
||||
val_p = Paragraph(disp, STYLES['FieldValue'])
|
||||
|
||||
@@ -419,8 +468,8 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
||||
|
||||
cells.append(box)
|
||||
col_widths.append(cell_w)
|
||||
has_content = True
|
||||
|
||||
# Skip the entire row if nothing made it through the filter
|
||||
if not cells:
|
||||
continue
|
||||
|
||||
|
||||
Reference in New Issue
Block a user