Skip to content

Email templating engine#34

Open
stefanotroncaro wants to merge 16 commits into
feat/email-service-asyncfrom
feat/email-templates
Open

Email templating engine#34
stefanotroncaro wants to merge 16 commits into
feat/email-service-asyncfrom
feat/email-templates

Conversation

@stefanotroncaro
Copy link
Copy Markdown
Contributor

@stefanotroncaro stefanotroncaro commented Nov 5, 2025

Create templates submodule

Features

  • jinja2 based rendering
  • mjml rendering via mrml library
  • inheritable BaseTemplate and BaseEmailTemplate pydantic schemas to relate template files and required arguments
  • flexible post rendering pipeline configurable on 3 levels: template definition, render method call, and service instance
  • component based template definition example, defining a theme to follow a design system for template consistency
  • module usage examples

Proof of testing

Sample email

image

Test suite

image

@stefanotroncaro stefanotroncaro marked this pull request as ready for review November 5, 2025 22:28
Comment thread app/emails/templates/_base.j2 Outdated
@stefanotroncaro stefanotroncaro marked this pull request as draft November 6, 2025 15:39
@stefanotroncaro stefanotroncaro marked this pull request as ready for review November 28, 2025 20:29
error_message=f"Sending new user email to user {user.id} failed",
from app.users.schemas.templates import NewUserTemplate

template = NewUserTemplate(name=user.email)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the template not being inyectable here. Is there a specific reason?
I'm thinking about maybe A/B testing where you want to send 2 different new user emails to users depending on X factor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree with you.

I want to add that functionality on a later refactor, to clean up the email service so that it is closed for modification (which it currently is not, as it basically requires a new method for each email type).

I didn't do it here because I didn't want to scope creep this PR. Would you be ok with working on that on a different PR?

@DanTcheche DanTcheche requested a review from Copilot January 26, 2026 14:02
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a comprehensive email templating system based on Jinja2 and MJML, replacing the previous simple HTML template approach. The new system provides a flexible, component-based architecture for creating and rendering email templates with consistent styling through a design system.

Changes:

  • Implemented a new templates module with base classes, services, and utilities for template rendering
  • Created a component-based email template system using Jinja2 and MJML with reusable atoms (text, image, heading) and molecules (section, column)
  • Migrated email service from file-based HTML templates to the new templating system with proper error handling

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pyproject.toml Added jinja2 and mjml-python dependencies
.python-version Updated Python version from 3.12.9 to 3.13.8
app/templates/services/templates_service.py Core service for rendering templates with configurable processing pipelines
app/templates/schemas/base_template.py Base schema for templates with path and pipeline configuration
app/templates/schemas/base_email_template.py Email-specific template schema with MJML rendering
app/emails/services/emails_service.py Refactored to use new template service instead of file-based templates
assets/templates/*.j2 New Jinja2 template files for layouts, components, and email content
app/users/schemas/templates.py Example template definition for new user emails
app/core/config.py Added "test" environment to RUN_ENV literal

{% from "constants/theme.j2" import theme %}
{% from "utils/render_attributes.j2" import render_attributes %}

{% macro text(variant='h1', styles={}) %}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macro is named 'text' but the file is 'heading.j2' and it renders heading variants (h1, h2, h3). The macro name should be 'heading' to match the file name and its purpose.

Suggested change
{% macro text(variant='h1', styles={}) %}
{% macro heading(variant='h1', styles={}) %}

Copilot uses AI. Check for mistakes.

Example:
```
class {cls_name}(BaseTemplate, path="path/to/template.mj"):
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected file extension from 'mj' to 'j2' in the example.

Suggested change
class {cls_name}(BaseTemplate, path="path/to/template.mj"):
class {cls_name}(BaseTemplate, path="path/to/template.j2"):

Copilot uses AI. Check for mistakes.
max_retries=settings.SEND_WELCOME_EMAIL_MAX_RETRIES,
backoff_in_seconds=settings.SEND_WELCOME_EMAIL_RETRY_BACKOFF_VALUE,
error_message=f"Sending new user email to user {user.id} failed",
from app.users.schemas.templates import NewUserTemplate
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement is placed inside the method rather than at the module level. Move this import to the top of the file with other imports to follow Python conventions and improve code clarity.

Copilot uses AI. Check for mistakes.
user.email,
Paths.NEW_USER.value,
"Welcome",
from app.users.schemas.templates import NewUserTemplate
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement is placed inside the method rather than at the module level. Move this import to the top of the file with other imports to follow Python conventions and improve code clarity.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,2 @@
from .invalid_template_path_exception import InvalidTemplatePathException
from .template_missing_path_exception import TemplateMissingPathException
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing all definition. Add all to explicitly declare the public API, consistent with other modules in the codebase.

Suggested change
from .template_missing_path_exception import TemplateMissingPathException
from .template_missing_path_exception import TemplateMissingPathException
__all__ = [
"InvalidTemplatePathException",
"TemplateMissingPathException",
]

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants