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)
|
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 ─────────────────────────────────────────────────────
|
# ── Render row by row ─────────────────────────────────────────────────────
|
||||||
for row_num, fields_in_row in rows_map.items():
|
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 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))
|
story.append(Spacer(1, 6))
|
||||||
sec_label = sections[row_num]
|
sec_label = sections[row_num]
|
||||||
# Bold letter prefix (e.g. "A.", "B.") + rest of label
|
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
sec_label,
|
sec_label,
|
||||||
ParagraphStyle('SecBanner', fontName='Helvetica-Bold',
|
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,
|
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 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 = []
|
cells = []
|
||||||
col_widths = []
|
col_widths = []
|
||||||
has_content = False
|
|
||||||
|
# Types that are always shown regardless of value
|
||||||
|
_ALWAYS_SHOW = {'text', 'textarea', 'label'}
|
||||||
|
|
||||||
for field in fields_in_row:
|
for field in fields_in_row:
|
||||||
ftype = field.get('type', '')
|
ftype = field.get('type', '')
|
||||||
col_span = field.get('colSpan', 1)
|
col_span = field.get('colSpan', 1)
|
||||||
cell_w = UNIT * col_span
|
cell_w = UNIT * col_span
|
||||||
|
|
||||||
|
# ── 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)
|
||||||
@@ -319,87 +364,91 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
)
|
)
|
||||||
cells.append(p)
|
cells.append(p)
|
||||||
col_widths.append(cell_w)
|
col_widths.append(cell_w)
|
||||||
has_content = True
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fid = str(field.get('id', ''))
|
fid = str(field.get('id', ''))
|
||||||
val = form_data.get(fid, '')
|
val = form_data.get(fid, '')
|
||||||
lbl = field.get('label', '')
|
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'])
|
lbl_p = Paragraph(lbl, STYLES['FieldLabel'])
|
||||||
|
|
||||||
# ── Value content ────────────────────────────────────────────────
|
# ── Value content ────────────────────────────────────────────────
|
||||||
if ftype == 'rating':
|
if ftype == 'rating':
|
||||||
score_int = int(val) if str(val).isdigit() else 0
|
score_int = int(val)
|
||||||
if score_int > 0:
|
stars = (f'<font color="#f59e0b">{"★" * score_int}'
|
||||||
stars = (f'<font color="#f59e0b">{"★" * score_int}'
|
f'{"☆" * (5 - score_int)}</font>'
|
||||||
f'{"☆" * (5 - score_int)}</font>'
|
f' <font color="#64748b">{score_int}/5</font>')
|
||||||
f' <font color="#64748b">{score_int}/5</font>')
|
|
||||||
else:
|
|
||||||
stars = '<font color="#94a3b8"><i>Not rated</i></font>'
|
|
||||||
val_p = Paragraph(stars, ParagraphStyle(
|
val_p = Paragraph(stars, ParagraphStyle(
|
||||||
'rv', fontName='Helvetica', fontSize=9, leading=12))
|
'rv', fontName='Helvetica', fontSize=9, leading=12))
|
||||||
|
|
||||||
elif ftype == 'image':
|
elif ftype == 'image':
|
||||||
if val:
|
img_path = os.path.join(static_folder, val)
|
||||||
img_path = os.path.join(static_folder, val)
|
if os.path.exists(img_path):
|
||||||
if os.path.exists(img_path):
|
try:
|
||||||
try:
|
val_p = RLImage(img_path,
|
||||||
val_p = RLImage(img_path,
|
width=min(cell_w - 12, 1.4 * inch),
|
||||||
width=min(cell_w - 12, 1.4 * inch),
|
height=1.0 * inch,
|
||||||
height=1.0 * inch,
|
kind='proportional')
|
||||||
kind='proportional')
|
except Exception:
|
||||||
except Exception:
|
|
||||||
val_p = Paragraph(
|
|
||||||
'<i><font color="#94a3b8">Photo error</font></i>',
|
|
||||||
STYLES['FieldValue'])
|
|
||||||
else:
|
|
||||||
val_p = Paragraph(
|
val_p = Paragraph(
|
||||||
'<i><font color="#94a3b8">No photo</font></i>',
|
'<i><font color="#94a3b8">Photo error</font></i>',
|
||||||
STYLES['FieldValue'])
|
STYLES['FieldValue'])
|
||||||
else:
|
else:
|
||||||
val_p = Paragraph(
|
val_p = Paragraph(
|
||||||
'<i><font color="#94a3b8">No photo</font></i>',
|
'<i><font color="#94a3b8">File not found</font></i>',
|
||||||
STYLES['FieldValue'])
|
STYLES['FieldValue'])
|
||||||
|
|
||||||
elif ftype == 'signature':
|
elif ftype == 'signature':
|
||||||
val_p = Paragraph(
|
val_p = Paragraph('[Signature captured]', STYLES['FieldValue'])
|
||||||
'[Signature captured]' if (val and str(val).startswith('data:'))
|
|
||||||
else '<i><font color="#94a3b8">No signature</font></i>',
|
|
||||||
STYLES['FieldValue'])
|
|
||||||
|
|
||||||
elif ftype == 'checkbox':
|
elif ftype == 'checkbox':
|
||||||
val_p = Paragraph('Yes' if val == 'yes' else 'No',
|
val_p = Paragraph('Yes', STYLES['FieldValue'])
|
||||||
STYLES['FieldValue'])
|
|
||||||
|
|
||||||
elif ftype == 'checkbox_group':
|
elif ftype == 'checkbox_group':
|
||||||
val_p = Paragraph(
|
val_p = Paragraph(', '.join(val), STYLES['FieldValue'])
|
||||||
', '.join(val) if (val and isinstance(val, list))
|
|
||||||
else '<i><font color="#94a3b8">None</font></i>',
|
|
||||||
STYLES['FieldValue'])
|
|
||||||
|
|
||||||
elif ftype == 'table':
|
elif ftype == 'table':
|
||||||
if val and isinstance(val, list) and len(val) > 0:
|
headers = field.get('col_headers',
|
||||||
headers = field.get('col_headers',
|
list(val[0].keys()) if val else [])
|
||||||
list(val[0].keys()) if val else [])
|
tbl_data = ([headers] +
|
||||||
tbl_data = ([headers] +
|
[[r.get(h, '') for h in headers] for r in val])
|
||||||
[[r.get(h, '') for h in headers] for r in val])
|
val_p = Table(tbl_data)
|
||||||
val_p = Table(tbl_data)
|
val_p.setStyle(TableStyle([
|
||||||
val_p.setStyle(TableStyle([
|
('BACKGROUND', (0, 0), (-1, 0), C_LIGHT),
|
||||||
('BACKGROUND', (0, 0), (-1, 0), C_LIGHT),
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
('FONTSIZE', (0, 0), (-1, -1), 7),
|
||||||
('FONTSIZE', (0, 0), (-1, -1), 7),
|
('INNERGRID', (0, 0), (-1, -1), 0.25, C_BORDER),
|
||||||
('INNERGRID', (0, 0), (-1, -1), 0.25, C_BORDER),
|
('BOX', (0, 0), (-1, -1), 0.5, C_BORDER),
|
||||||
('BOX', (0, 0), (-1, -1), 0.5, C_BORDER),
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
||||||
('TOPPADDING', (0, 0), (-1, -1), 2),
|
('BOTTOMPADDING', (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'])
|
|
||||||
|
|
||||||
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'])
|
||||||
|
|
||||||
@@ -419,8 +468,8 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
|||||||
|
|
||||||
cells.append(box)
|
cells.append(box)
|
||||||
col_widths.append(cell_w)
|
col_widths.append(cell_w)
|
||||||
has_content = True
|
|
||||||
|
|
||||||
|
# Skip the entire row if nothing made it through the filter
|
||||||
if not cells:
|
if not cells:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user