From 6bd4b71588f4332503334405ee18451d6b8c1c00 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Thu, 25 Dec 2025 22:25:33 +0100 Subject: [PATCH] fix: use table_header_custom for custom headers in subscription pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table_header() macro doesn't support caller() - it takes a columns list. Using {% call table_header() %} caused a Jinja2 error: "macro 'table_header' was invoked with two values for the special caller argument" Changes: - Add table_header_custom() macro that supports caller() for custom headers - Update subscriptions.html, subscription-tiers.html, billing-history.html - Add TPL-008 architecture rule to detect this pattern - Renumber TPL-009 (block names) and TPL-010 (Alpine vars) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .architecture-rules/frontend.yaml | 31 ++++++++++++ app/templates/admin/billing-history.html | 4 +- app/templates/admin/subscription-tiers.html | 4 +- app/templates/admin/subscriptions.html | 4 +- app/templates/shared/macros/tables.html | 21 ++++++++ scripts/validate_architecture.py | 54 +++++++++++++++++---- 6 files changed, 103 insertions(+), 15 deletions(-) diff --git a/.architecture-rules/frontend.yaml b/.architecture-rules/frontend.yaml index 4189a5a4..b33dad69 100644 --- a/.architecture-rules/frontend.yaml +++ b/.architecture-rules/frontend.yaml @@ -381,6 +381,37 @@ template_rules: recommended_pattern: '' - id: "TPL-008" + name: "Use table_header_custom for custom headers, not table_header" + severity: "error" + description: | + When using {% call %} to create custom table headers with th_sortable + or custom elements, you MUST use table_header_custom(), not table_header(). + + The table_header() macro takes a columns list and does NOT support caller(). + Using {% call table_header() %} causes a Jinja2 error: + "macro 'table_header' was invoked with two values for the special caller argument" + + WRONG (causes 500 error): + {% call table_header() %} + {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }} + Actions + {% endcall %} + + RIGHT (supports caller): + {% call table_header_custom() %} + {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }} + Actions + {% endcall %} + + OR for simple headers (list of column names): + {{ table_header(['Name', 'Email', 'Status', 'Actions']) }} + pattern: + file_pattern: "app/templates/**/*.html" + anti_patterns: + - "{%\\s*call\\s+table_header\\s*\\(\\s*\\)\\s*%}" + required_alternative: "table_header_custom" + + - id: "TPL-009" name: "Use valid block names from base templates" severity: "error" description: | diff --git a/app/templates/admin/billing-history.html b/app/templates/admin/billing-history.html index cfdda5e7..21048f6c 100644 --- a/app/templates/admin/billing-history.html +++ b/app/templates/admin/billing-history.html @@ -2,7 +2,7 @@ {% extends "admin/base.html" %} {% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} {% from 'shared/macros/headers.html' import page_header_refresh %} -{% from 'shared/macros/tables.html' import table_wrapper, table_header, th_sortable %} +{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable %} {% from 'shared/macros/pagination.html' import pagination_full %} {% block title %}Billing History{% endblock %} @@ -107,7 +107,7 @@ {% call table_wrapper() %} - {% call table_header() %} + {% call table_header_custom() %} {{ th_sortable('invoice_date', 'Date', 'sortBy', 'sortOrder') }} {{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }} diff --git a/app/templates/admin/subscription-tiers.html b/app/templates/admin/subscription-tiers.html index cd83f916..6aa5402b 100644 --- a/app/templates/admin/subscription-tiers.html +++ b/app/templates/admin/subscription-tiers.html @@ -3,7 +3,7 @@ {% extends "admin/base.html" %} {% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} {% from 'shared/macros/headers.html' import page_header_refresh %} -{% from 'shared/macros/tables.html' import table_wrapper, table_header, th_sortable, empty_state %} +{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable, empty_state %} {% from 'shared/macros/modals.html' import modal_confirm %} {% block title %}Subscription Tiers{% endblock %} @@ -85,7 +85,7 @@ {% call table_wrapper() %}
Invoice #
- {% call table_header() %} + {% call table_header_custom() %} {{ th_sortable('code', 'Code', 'sortBy', 'sortOrder') }} {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }} diff --git a/app/templates/admin/subscriptions.html b/app/templates/admin/subscriptions.html index ddccef39..458f1527 100644 --- a/app/templates/admin/subscriptions.html +++ b/app/templates/admin/subscriptions.html @@ -2,7 +2,7 @@ {% extends "admin/base.html" %} {% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} {% from 'shared/macros/headers.html' import page_header_refresh %} -{% from 'shared/macros/tables.html' import table_wrapper, table_header, th_sortable %} +{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable %} {% from 'shared/macros/pagination.html' import pagination_full %} {% block title %}Vendor Subscriptions{% endblock %} @@ -139,7 +139,7 @@ {% call table_wrapper() %}
#
- {% call table_header() %} + {% call table_header_custom() %} {{ th_sortable('vendor_name', 'Vendor', 'sortBy', 'sortOrder') }} {{ th_sortable('tier', 'Tier', 'sortBy', 'sortOrder') }} {{ th_sortable('status', 'Status', 'sortBy', 'sortOrder') }} diff --git a/app/templates/shared/macros/tables.html b/app/templates/shared/macros/tables.html index f1af74a0..2940986b 100644 --- a/app/templates/shared/macros/tables.html +++ b/app/templates/shared/macros/tables.html @@ -48,6 +48,27 @@ {% endmacro %} +{# + Custom Table Header + =================== + Renders a table header with custom content via caller(). + Use this when you need th_sortable or custom th elements. + + Usage: + {% call table_header_custom() %} + {{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }} + + {% endcall %} +#} +{% macro table_header_custom() %} + + + {{ caller() }} + + +{% endmacro %} + + {# Sortable Table Header ===================== diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index b8046aef..686c6783 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -813,7 +813,10 @@ class ArchitectureValidator: # TPL-007: Check empty state implementation self._check_template_empty_state(file_path, content, lines) - # TPL-008: Check for invalid block names + # TPL-008: Check for call table_header() pattern (should be table_header_custom) + self._check_table_header_call_pattern(file_path, content, lines) + + # TPL-009: Check for invalid block names if is_admin: self._check_valid_block_names(file_path, content, lines) @@ -1058,11 +1061,41 @@ class ArchitectureValidator: suggestion='Add ', ) + def _check_table_header_call_pattern( + self, file_path: Path, content: str, lines: list[str] + ): + """TPL-008: Check that {% call table_header() %} is not used. + + The table_header() macro doesn't support caller() - it takes a columns list. + Using {% call table_header() %} causes a Jinja2 error: + "macro 'table_header' was invoked with two values for the special caller argument" + + Use table_header_custom() instead for custom headers with caller(). + """ + if "noqa: tpl-008" in content.lower(): + return + + import re + pattern = re.compile(r"{%\s*call\s+table_header\s*\(\s*\)\s*%}") + + for i, line in enumerate(lines, 1): + if pattern.search(line): + self._add_violation( + rule_id="TPL-008", + rule_name="Use table_header_custom for custom headers", + severity=Severity.ERROR, + file_path=file_path, + line_number=i, + message="{% call table_header() %} causes Jinja2 error - table_header doesn't support caller()", + context=line.strip()[:80], + suggestion="Use {% call table_header_custom() %} for custom headers with th_sortable or custom
Actions
elements", + ) + def _check_valid_block_names( self, file_path: Path, content: str, lines: list[str] ): - """TPL-008: Check that templates use valid block names from base template""" - if "noqa: tpl-008" in content.lower(): + """TPL-009: Check that templates use valid block names from base template""" + if "noqa: tpl-009" in content.lower(): return # Skip base templates @@ -1092,7 +1125,7 @@ class ArchitectureValidator: block_name = match.group(1) if block_name in invalid_blocks: self._add_violation( - rule_id="TPL-008", + rule_id="TPL-009", rule_name="Use valid block names from base templates", severity=Severity.ERROR, file_path=file_path, @@ -1105,8 +1138,8 @@ class ArchitectureValidator: def _check_alpine_template_vars( self, file_path: Path, content: str, lines: list[str], js_content: str ): - """TPL-009: Check that Alpine variables used in templates are defined in JS""" - if "noqa: tpl-009" in content.lower(): + """TPL-010: Check that Alpine variables used in templates are defined in JS""" + if "noqa: tpl-010" in content.lower(): return import re @@ -1141,7 +1174,7 @@ class ArchitectureValidator: for i, line in enumerate(lines, 1): if var in line and ("error_state" in line or "action_dropdown" in line): self._add_violation( - rule_id="TPL-009", + rule_id="TPL-010", rule_name="Alpine variables must be defined in JS component", severity=Severity.ERROR, file_path=file_path, @@ -2913,11 +2946,14 @@ class ArchitectureValidator: if not is_base_or_partial and not is_macro and not is_components_page: self._check_number_stepper_macro_usage(file_path, content, lines) - # TPL-008: Check for invalid block names + # TPL-008: Check for call table_header() pattern + self._check_table_header_call_pattern(file_path, content, lines) + + # TPL-009: Check for invalid block names if not is_base_or_partial: self._check_valid_block_names(file_path, content, lines) - # TPL-009: Check Alpine variables are defined in JS + # TPL-010: Check Alpine variables are defined in JS if not is_base_or_partial and not is_macro and not is_components_page: # Try to find corresponding JS file # Template: app/templates/admin/messages.html -> JS: static/admin/js/messages.js