Template Structure
The application uses Jinja2 as the templating engine, organized by module with a shared base template.
templates/
├── base.html # Base layout (navigation, flash messages)
├── colors/ # Color module templates
│ ├── list.html
│ ├── create.html
│ └── edit.html
├── wood_types/ # Wood types templates
├── furniture_types/ # Furniture types templates
├── roles/ # Roles templates
├── unit_of_measures/ # Unit of measures templates
└── errors/ # Error pages
└── error.html
Base Template
All templates extend from base.html, which provides the common layout:
<! doctype html >
< html lang = "es" >
< head >
< meta charset = "UTF-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
< title > {% block title %}Furniture Store{% endblock %} </ title >
</ head >
< body >
< nav >
< a href = "{{ url_for('colors.list_colors') }}" > Colores </ a > |
< a href = "{{ url_for('colors.create_color') }}" > Crear Color </ a > |
< a href = "{{ url_for('roles.list_roles') }}" > Roles </ a > |
<!-- More navigation links -->
</ nav >
< hr />
< main >
{# Flash messages #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
< p role = "alert"
style = "color: {{ 'green' if category == 'success' else 'red' }};" >
< strong > {{ 'Success:' if category == 'success' else 'Error:' }} </ strong >
{{ message }}
</ p >
{% endfor %}
{% endif %}
{% endwith %}
{# Page content #}
{% block content %}{% endblock %}
</ main >
</ body >
</ html >
Base Template Features
Navigation Consistent navigation across all pages
Flash Messages Display success/error notifications
Content Block Placeholder for page-specific content
Extending the Base Template
All page templates extend base.html using template inheritance:
app/templates/colors/create.html
{% extends "base.html" %}
{% block title %}Create Color - Furniture Store{% endblock %}
{% block content %}
< h1 > Add New Color </ h1 >
< form method = "POST" action = "{{ url_for('colors.create_color') }}" >
{{ form.hidden_tag() }}
{{ form.name.label }}
{{ form.name(size=30) }}
{% for error in form.name.errors %}
< p style = "color: red;" > {{ error }} </ p >
{% endfor %}
< button type = "submit" > Create </ button >
</ form >
{% endblock %}
Template Blocks
Title Block
Customize the page title:
{% block title %}Your Page Title - Furniture Store{% endblock %}
Content Block
Main page content:
{% block content %}
<!-- Your page content here -->
{% endblock %}
Render WTForms in templates:
< form method = "POST" action = "{{ url_for('colors.create_color') }}" >
{# CSRF token - REQUIRED #}
{{ form.hidden_tag() }}
{# Field label #}
{{ form.name.label }}
{# Field input #}
{{ form.name(size=30) }}
{# Display validation errors #}
{% for error in form.name.errors %}
< p style = "color: red;" > {{ error }} </ p >
{% endfor %}
< button type = "submit" > Submit </ button >
</ form >
Always include {{ form.hidden_tag() }} for CSRF protection. Forms will fail without it.
Display field-specific validation errors:
{% for error in form.name.errors %}
< p style = "color: red;" > {{ error }} </ p >
{% endfor %}
For edit forms, populate fields on GET requests:
app/catalogs/colors/routes.py
@colors_bp.route ( '/<int:id_color>/edit' , methods = [ 'GET' , 'POST' ])
def edit_color ( id_color : int ):
color = ColorService.get_by_id(id_color)
form = ColorForm()
if form.validate_on_submit():
# Handle POST
pass
elif request.method == 'GET' :
# Pre-populate form on GET
form.name.data = color.name
return render_template( 'colors/edit.html' , form = form, color = color)
Flash Messages
Setting Flash Messages
In routes, use flash() to send messages to the user:
from flask import flash
# Success message
flash( 'Color created successfully' , 'success' )
# Error message
flash( 'Color name already exists' , 'error' )
Displaying Flash Messages
Flash messages are displayed in base.html:
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
< p role = "alert"
style = "color: {{ 'green' if category == 'success' else 'red' }};" >
< strong > {{ 'Success:' if category == 'success' else 'Error:' }} </ strong >
{{ message }}
</ p >
{% endfor %}
{% endif %}
{% endwith %}
Flash Message Categories
Category Color Usage successGreen Successful operations errorRed Errors and failures
URL Generation
Using url_for()
Always use url_for() to generate URLs. Never hardcode paths.
{# Basic route #}
< a href = "{{ url_for('colors.list_colors') }}" > View Colors </ a >
{# Route with parameter #}
< a href = "{{ url_for('colors.edit_color', id_color=color.id_color) }}" > Edit </ a >
{# Form action #}
< form method = "POST" action = "{{ url_for('colors.create_color') }}" >
...
</ form >
List Templates
Displaying Data Tables
Example from colors/list.html:
app/templates/colors/list.html
{% extends "base.html" %}
{% block title %}Colors - Furniture Store{% endblock %}
{% block content %}
< h1 > Color Catalog </ h1 >
< a href = "{{ url_for('colors.create_color') }}" > Add New Color </ a >
< h2 > Color List </ h2 >
{% if colors %}
< table class = "colors-table" >
< thead >
< tr >
< th scope = "col" > ID </ th >
< th scope = "col" > Name </ th >
< th scope = "col" > Active </ th >
< th scope = "col" > Created </ th >
< th scope = "col" > Actions </ th >
</ tr >
</ thead >
< tbody >
{% for color in colors %}
< tr >
< td > {{ color.id_color }} </ td >
< td > {{ color.name }} </ td >
< td > {{ "Yes" if color.active else "No" }} </ td >
< td > {{ color.created_at.strftime('%Y-%m-%d %H:%M') if color.created_at else 'N/A' }} </ td >
< td >
< a href = "{{ url_for('colors.edit_color', id_color=color.id_color) }}" > Edit </ a >
{# Delete form #}
< form method = "POST"
action = "{{ url_for('colors.delete_color', id_color=color.id_color) }}"
style = "display:inline;"
onsubmit = " return confirm ('Are you sure you want to delete this color?');" >
< input type = "hidden" name = "csrf_token" value = "{{ csrf_token() }}" />
< button type = "submit" style = "color: red;" > Delete </ button >
</ form >
</ td >
</ tr >
{% endfor %}
</ tbody >
</ table >
{% else %}
< p > No colors registered. </ p >
{% endif %}
{% endblock %}
For delete actions, use inline forms with CSRF protection:
< form method = "POST"
action = "{{ url_for('colors.delete_color', id_color=color.id_color) }}"
style = "display:inline;"
onsubmit = " return confirm ('Are you sure?');" >
{# CSRF token for inline forms #}
< input type = "hidden" name = "csrf_token" value = "{{ csrf_token() }}" />
< button type = "submit" style = "color: red;" > Delete </ button >
</ form >
For inline delete forms, use {{ csrf_token() }} instead of {{ form.hidden_tag() }}.
Conditional Rendering
If/Else Statements
{% if colors %}
< table > ... </ table >
{% else %}
< p > No colors available. </ p >
{% endif %}
Ternary Expressions
< td > {{ "Yes" if color.active else "No" }} </ td >
Loops
For Loops
{% for color in colors %}
< tr >
< td > {{ color.name }} </ td >
</ tr >
{% endfor %}
Loop Variables
{% for color in colors %}
< tr class = "{{ 'odd' if loop.index % 2 else 'even' }}" >
< td > {{ loop.index }}. {{ color.name }} </ td >
</ tr >
{% endfor %}
Available loop variables:
loop.index - Current iteration (1-indexed)
loop.index0 - Current iteration (0-indexed)
loop.first - True if first iteration
loop.last - True if last iteration
{# This is a Jinja2 comment - not rendered in HTML #}
<!-- This is an HTML comment - visible in page source -->
Filters
Common Filters
{# String formatting #}
{{ color.name|upper }}
{{ color.name|lower }}
{{ color.name|title }}
{# Date formatting #}
{{ color.created_at.strftime('%Y-%m-%d %H:%M') }}
{# Default value #}
{{ color.description|default('No description') }}
{# Safe HTML rendering #}
{{ html_content|safe }}
Template Best Practices
Ensures consistent layout and navigation across all pages. {% extends "base.html" %}
Always include {{ form.hidden_tag() }} in forms. < form method = "POST" >
{{ form.hidden_tag() }}
</ form >
Never hardcode URLs. Use url_for() for all links and form actions. < a href = "{{ url_for('colors.list_colors') }}" > Colors </ a >
Check if data exists before rendering tables. {% if colors %}
< table > ... </ table >
{% else %}
< p > No colors available. </ p >
{% endif %}
Next Steps
Forms Guide Learn WTForms validation patterns
Coding Conventions Follow coding standards
Project Structure Understand the architecture
Environment Variables Configure your environment