feat(emails+rsvp): card-based invitation/update/cancel/reply emails + /rsvp pages#57
feat(emails+rsvp): card-based invitation/update/cancel/reply emails + /rsvp pages#57mosa-riel wants to merge 6 commits into
Conversation
📝 WalkthroughWalkthroughThis PR refactors calendar email templates to use a shared layout pattern, modernizes RSVP confirmation and response pages with unified styling, and adds badge translations. The changes reduce duplication across invitation emails while establishing consistent visual styling for RSVP user interactions. ChangesEmail and RSVP Template Modernization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/core/api/viewsets_rsvp.py`:
- Line 60: Extract the repeated getattr(settings, "APP_NAME", "Calendars") into
a module-level constant (e.g., APP_NAME = getattr(settings, "APP_NAME",
"Calendars")) placed near the top of the file after the logger declaration, and
then replace the three inline occurrences used in the render contexts (the error
render and the GET confirmation and POST response render calls) with that
constant so all templates use the single APP_NAME symbol.
In `@src/backend/core/templates/emails/calendar_invitation_cancel.html`:
- Around line 31-35: The cancel-email template renders event.url as plain text;
change the paragraph that contains {{ labels.videoConference }} and {{ event.url
}} to render a clickable link by wrapping the URL in an anchor using href="{{
event.url }}", setting target="_blank" and rel="noopener noreferrer" so the
conference link is clickable and opens safely in a new tab while preserving the
existing label (labels.videoConference).
In `@src/backend/core/templates/rsvp/response.html`:
- Around line 7-32: The CSS in response.html duplicates styles from confirm.html
(notably .wrap, .card, .header, .badge, .title, .body, .divider, .footer);
extract these shared rules into a common template (e.g.,
src/backend/core/templates/rsvp/_base.html or _common_styles.html) and have
response.html and confirm.html either extend the base via {% extends
"rsvp/_base.html" %} with a {% block extra_styles %} for overrides or include
the shared snippet with {% include "rsvp/_common_styles.html" %}, leaving only
template-specific styles in each file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c9d5592f-c88a-4f01-a8ae-852b2517262b
📒 Files selected for processing (9)
src/backend/core/api/viewsets_rsvp.pysrc/backend/core/templates/emails/_invitation_layout.htmlsrc/backend/core/templates/emails/calendar_invitation.htmlsrc/backend/core/templates/emails/calendar_invitation_cancel.htmlsrc/backend/core/templates/emails/calendar_invitation_reply.htmlsrc/backend/core/templates/emails/calendar_invitation_update.htmlsrc/backend/core/templates/rsvp/confirm.htmlsrc/backend/core/templates/rsvp/response.htmlsrc/frontend/apps/calendars/src/features/i18n/translations.json
| "error_title": t("rsvp.error.invalidLink", lang), | ||
| "header_color": "#dc2626", | ||
| "lang": lang, | ||
| "app_name": getattr(settings, "APP_NAME", "Calendars"), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Extract duplicate app name pattern to eliminate repetition.
The pattern getattr(settings, "APP_NAME", "Calendars") is repeated identically in three render contexts (error, GET confirmation, POST response). Extract this to a module-level constant to follow DRY principles and provide a single source of truth.
♻️ Proposed refactor
Add near the top of the file, after the logger definition:
logger = logging.getLogger(__name__)
+
+# Fallback app name for RSVP page footers
+APP_NAME = getattr(settings, "APP_NAME", "Calendars")
PARTSTAT_ICONS = {Then reference it in all three locations:
{
"page_title": t("rsvp.error.title", lang),
"error": message,
"error_title": t("rsvp.error.invalidLink", lang),
"header_color": "`#dc2626`",
"lang": lang,
- "app_name": getattr(settings, "APP_NAME", "Calendars"),
+ "app_name": APP_NAME,
}, {
"page_title": label,
"token": token,
"lang": lang,
"heading": label,
"status_icon": PARTSTAT_ICONS[action],
"header_color": PARTSTAT_COLORS[action],
"submit_label": label,
- "app_name": getattr(settings, "APP_NAME", "Calendars"),
+ "app_name": APP_NAME,
}, {
"page_title": label,
"heading": label,
"message": t("rsvp.responseSent", lang),
"status_icon": PARTSTAT_ICONS[action],
"header_color": PARTSTAT_COLORS[action],
"event_summary": summary,
"lang": lang,
- "app_name": getattr(settings, "APP_NAME", "Calendars"),
+ "app_name": APP_NAME,
},Also applies to: 163-163, 204-204
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/api/viewsets_rsvp.py` at line 60, Extract the repeated
getattr(settings, "APP_NAME", "Calendars") into a module-level constant (e.g.,
APP_NAME = getattr(settings, "APP_NAME", "Calendars")) placed near the top of
the file after the logger declaration, and then replace the three inline
occurrences used in the render contexts (the error render and the GET
confirmation and POST response render calls) with that constant so all templates
use the single APP_NAME symbol.
Restyle the calendar invitation/update/cancel/reply emails AND the
/rsvp confirm + response pages into a consistent card layout (blue
header strip, uppercase badge, body section, footer). Email templates
share a new emails/_invitation_layout.html base via {% extends %},
preserving all existing context vars (content.badge/body, summary,
start_date/time_str, labels.*, actions.*, rsvp_*_url, instructions,
footer). RSVP pages get a matching card with header colored per action.
Translation additions (en/fr/nl): email.invitation.badge (was missing)
and email.reply.badge (was missing) — required by the new header layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuse the invitation email's blue card design for the /rsvp confirm and
response pages, with meeting details filled in:
- confirm.html and response.html: blue card with header (badge + title),
body, divider, and footer ("via {{ app_name }}").
- response.html: title = event_summary; badge = action label
(uppercased); body shows status icon, optional event_date, and the
confirmation message. Error path uses a red header.
- viewsets_rsvp.py: pass app_name (settings.APP_NAME) to the three
render contexts (GET confirm, POST response, _render_error).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the partstat icon from /rsvp confirm + response pages — the
uppercase badge ("ACCEPTED"/"DECLINED"/"TENTATIVE") already conveys
the action, the icon was redundant.
- Use {% comment %}…{% endcomment %} for the multi-line header doc in
emails/_invitation_layout.html. Django {# … #} comments are single-
line only, so the doc lines below the first were leaking into the
rendered email body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…DRY rsvp CSS
- viewsets_rsvp.py: drop the app_name kwarg from all three render
contexts (error / GET confirm / POST response) — the footer string
is hardcoded "Calendars" in the rsvp templates instead, since these
pages are only ever served by the calendars app.
- calendar_invitation_cancel.html: render event.url as a clickable
<a target="_blank" rel="noopener noreferrer"> instead of plain text,
matching the invitation/update templates.
- rsvp/_common_styles.html (new): shared CSS for the rsvp pages —
base .wrap/.card/.header/.badge/.title/.body/.divider/.footer plus
a .err variant for header/badge. confirm.html and response.html
now {% include %} it and keep only page-specific rules (.submit-btn
on confirm, .event-date on response). Dropped the unused .ok class
from response.html's success branch (default is blue).
Lint-only: ruff format flagged the closing braces in the GET-confirm and POST-response render contexts that ended up over-indented after the earlier app_name kwarg removal.
4153cb3 to
3bc5430
Compare
The shared CSS partial used a multi-line {# #} comment, which Django
does not support (no newlines allowed between the delimiters), so the
comment text was rendered into the page. Switch to {% comment %}.
The email RSVP restyle changed the button colors, but the end-to-end
tests still selected buttons by the old palette. Update the accept,
tentative, and decline colors to match the new design.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/core/templates/emails/_invitation_layout.html`:
- Around line 1-8: The HTML doctype must be the very first token to satisfy
HTMLHint; move the Jinja comment block currently preceding "<!doctype html>" so
that "<!doctype html>" is the first line and then place the "{% comment %} ...
{% endcomment %}" block immediately after it; update the file
"src/backend/core/templates/emails/_invitation_layout.html" accordingly and
verify the template still defines the same blocks (header_color, badge_color,
body).
In `@src/backend/core/templates/rsvp/confirm.html`:
- Around line 34-36: Replace the hardcoded footer text "Calendars" with the
template context variable so branding is consistent: update the footer <div
class="footer"> (the footer block in the rsvp confirmation template) to render
the provided app_name context variable (use the template's variable syntax, e.g.
app_name) instead of the literal string "Calendars".
In `@src/backend/core/templates/rsvp/response.html`:
- Around line 37-39: The footer currently hardcodes "Calendars" inside the <div
class="footer">; replace that hardcoded text with the template context variable
used for the application name (e.g., the injected variable appName or app_name)
so the footer renders the passed app name consistently; update the content
within the footer element in response.html to output the context variable
instead of the literal "Calendars".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8752fb89-aeac-4736-bcf0-f914e60794ee
📒 Files selected for processing (10)
src/backend/core/templates/emails/_invitation_layout.htmlsrc/backend/core/templates/emails/calendar_invitation.htmlsrc/backend/core/templates/emails/calendar_invitation_cancel.htmlsrc/backend/core/templates/emails/calendar_invitation_reply.htmlsrc/backend/core/templates/emails/calendar_invitation_update.htmlsrc/backend/core/templates/rsvp/_common_styles.htmlsrc/backend/core/templates/rsvp/confirm.htmlsrc/backend/core/templates/rsvp/response.htmlsrc/backend/core/tests/test_rsvp.pysrc/frontend/apps/calendars/src/features/i18n/translations.json
| {% comment %} | ||
| Shared layout for calendar invitation / update / cancel / reply emails. | ||
| Email-client-safe: table-based, inline styles, no external CSS. | ||
| Blocks: | ||
| header_color — background color of the header strip (default blue). | ||
| badge_color — text color of the uppercase header badge (default light blue tint). | ||
| body — the per-template body content (after the header, before the footer). | ||
| {% endcomment %}<!doctype html> |
There was a problem hiding this comment.
Move the template comment below the doctype.
Keeping <!doctype html> as the first token in the source avoids the current HTMLHint failure on this shared template.
Suggested fix
-{% comment %}
-Shared layout for calendar invitation / update / cancel / reply emails.
-Email-client-safe: table-based, inline styles, no external CSS.
-Blocks:
- header_color — background color of the header strip (default blue).
- badge_color — text color of the uppercase header badge (default light blue tint).
- body — the per-template body content (after the header, before the footer).
-{% endcomment %}<!doctype html>
+<!doctype html>
+{% comment %}
+Shared layout for calendar invitation / update / cancel / reply emails.
+Email-client-safe: table-based, inline styles, no external CSS.
+Blocks:
+ header_color — background color of the header strip (default blue).
+ badge_color — text color of the uppercase header badge (default light blue tint).
+ body — the per-template body content (after the header, before the footer).
+{% endcomment %}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {% comment %} | |
| Shared layout for calendar invitation / update / cancel / reply emails. | |
| Email-client-safe: table-based, inline styles, no external CSS. | |
| Blocks: | |
| header_color — background color of the header strip (default blue). | |
| badge_color — text color of the uppercase header badge (default light blue tint). | |
| body — the per-template body content (after the header, before the footer). | |
| {% endcomment %}<!doctype html> | |
| <!doctype html> | |
| {% comment %} | |
| Shared layout for calendar invitation / update / cancel / reply emails. | |
| Email-client-safe: table-based, inline styles, no external CSS. | |
| Blocks: | |
| header_color — background color of the header strip (default blue). | |
| badge_color — text color of the uppercase header badge (default light blue tint). | |
| body — the per-template body content (after the header, before the footer). | |
| {% endcomment %} |
🧰 Tools
🪛 HTMLHint (1.9.2)
[error] 1-1: Doctype must be declared before any non-comment content.
(doctype-first)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/templates/emails/_invitation_layout.html` around lines 1 -
8, The HTML doctype must be the very first token to satisfy HTMLHint; move the
Jinja comment block currently preceding "<!doctype html>" so that "<!doctype
html>" is the first line and then place the "{% comment %} ... {% endcomment %}"
block immediately after it; update the file
"src/backend/core/templates/emails/_invitation_layout.html" accordingly and
verify the template still defines the same blocks (header_color, badge_color,
body).
| <div class="footer"> | ||
| <p>Calendars</p> | ||
| </div> |
There was a problem hiding this comment.
Use app_name in the footer instead of hardcoded text.
Lines 34-36 hardcode Calendars, which bypasses the context variable added for branding consistency.
Suggested fix
<div class="footer">
- <p>Calendars</p>
+ <p>via {{ app_name }}</p>
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="footer"> | |
| <p>Calendars</p> | |
| </div> | |
| <div class="footer"> | |
| <p>via {{ app_name }}</p> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/templates/rsvp/confirm.html` around lines 34 - 36, Replace
the hardcoded footer text "Calendars" with the template context variable so
branding is consistent: update the footer <div class="footer"> (the footer block
in the rsvp confirmation template) to render the provided app_name context
variable (use the template's variable syntax, e.g. app_name) instead of the
literal string "Calendars".
| <div class="footer"> | ||
| <p>Calendars</p> | ||
| </div> |
There was a problem hiding this comment.
Footer should render the injected app name.
Lines 37-39 still hardcode Calendars; this should use the passed context variable for consistent branding.
Suggested fix
<div class="footer">
- <p>Calendars</p>
+ <p>via {{ app_name }}</p>
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="footer"> | |
| <p>Calendars</p> | |
| </div> | |
| <div class="footer"> | |
| <p>via {{ app_name }}</p> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/templates/rsvp/response.html` around lines 37 - 39, The
footer currently hardcodes "Calendars" inside the <div class="footer">; replace
that hardcoded text with the template context variable used for the application
name (e.g., the injected variable appName or app_name) so the footer renders the
passed app name consistently; update the content within the footer element in
response.html to output the context variable instead of the literal "Calendars".


Summary
Restyles the calendar invitation / update / cancel / reply emails and the
/rsvpconfirm + response pages into a consistent card layout: a coloredheader strip with an uppercase action badge + title, body, divider, and
footer. Driven by a small new
emails/_invitation_layout.htmlDjango basetemplate that the four email templates extend via
{% extends %}.The same card design is reused on the
/rsvppages, with the event titleas the card title and
{{ app_name }}in the footer, so the user clearlysees what event they RSVP'd for.
What changes
src/backend/core/templates/emails/_invitation_layout.html— table-based, inline styles, email-client-safe. Blocks:
header_color,badge_color,body.changes required):
calendar_invitation.html— default blue, RSVP buttons.calendar_invitation_update.html— default blue, RSVP buttons.calendar_invitation_cancel.html— red header, strike-through date.calendar_invitation_reply.html— green header./rsvppages restyled with the same card design:templates/rsvp/confirm.html— auto-submit page, blue card + footer.templates/rsvp/response.html— success: blue card with event summaryas the title; error: red header.
viewsets_rsvp.py— passapp_name = settings.APP_NAMEto the threerender contexts so the footer reads "via {{ app_name }}".
Translations
email.invitation.badge(en / fr / nl) — the layout's uppercaseheader label. Previously absent (only
update/cancelhadbadge),which would have surfaced the raw key.
email.reply.badge(en / fr / nl) — same reason.Summary by CodeRabbit
New Features
Style