Skip to main content

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:
app/templates/base.html
<!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 %}

Working with Forms

Form Rendering

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.

Form Field Errors

Display field-specific validation errors:
{% for error in form.name.errors %}
  <p style="color: red;">{{ error }}</p>
{% endfor %}

Pre-populating Forms (Edit)

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

CategoryColorUsage
successGreenSuccessful operations
errorRedErrors 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 %}

Delete Forms with CSRF

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

Comments

{# 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>
Show validation errors to help users correct input.
{% for error in form.name.errors %}
  <p style="color: red;">{{ error }}</p>
{% endfor %}
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

Build docs developers (and LLMs) love