Email templating engine#34
Conversation
Build/bump python to 3.13.8
| error_message=f"Sending new user email to user {user.id} failed", | ||
| from app.users.schemas.templates import NewUserTemplate | ||
|
|
||
| template = NewUserTemplate(name=user.email) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
templatesmodule 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={}) %} |
There was a problem hiding this comment.
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.
| {% macro text(variant='h1', styles={}) %} | |
| {% macro heading(variant='h1', styles={}) %} |
|
|
||
| Example: | ||
| ``` | ||
| class {cls_name}(BaseTemplate, path="path/to/template.mj"): |
There was a problem hiding this comment.
Corrected file extension from 'mj' to 'j2' in the example.
| class {cls_name}(BaseTemplate, path="path/to/template.mj"): | |
| class {cls_name}(BaseTemplate, path="path/to/template.j2"): |
| 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 |
There was a problem hiding this comment.
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.
| user.email, | ||
| Paths.NEW_USER.value, | ||
| "Welcome", | ||
| from app.users.schemas.templates import NewUserTemplate |
There was a problem hiding this comment.
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.
| @@ -0,0 +1,2 @@ | |||
| from .invalid_template_path_exception import InvalidTemplatePathException | |||
| from .template_missing_path_exception import TemplateMissingPathException | |||
There was a problem hiding this comment.
Missing all definition. Add all to explicitly declare the public API, consistent with other modules in the codebase.
| from .template_missing_path_exception import TemplateMissingPathException | |
| from .template_missing_path_exception import TemplateMissingPathException | |
| __all__ = [ | |
| "InvalidTemplatePathException", | |
| "TemplateMissingPathException", | |
| ] |
Create templates submodule
Features
BaseTemplateandBaseEmailTemplatepydantic schemas to relate template files and required argumentsProof of testing
Sample email
Test suite