Skip to content

feat(emails+rsvp): card-based invitation/update/cancel/reply emails + /rsvp pages#57

Open
mosa-riel wants to merge 6 commits into
suitenumerique:mainfrom
mosacloud:feat/email-rsvp-restyle
Open

feat(emails+rsvp): card-based invitation/update/cancel/reply emails + /rsvp pages#57
mosa-riel wants to merge 6 commits into
suitenumerique:mainfrom
mosacloud:feat/email-rsvp-restyle

Conversation

@mosa-riel
Copy link
Copy Markdown
Collaborator

@mosa-riel mosa-riel commented May 28, 2026

Summary

Restyles the calendar invitation / update / cancel / reply emails and the
/rsvp confirm + response pages into a consistent card layout: a colored
header strip with an uppercase action badge + title, body, divider, and
footer. Driven by a small new emails/_invitation_layout.html Django base
template that the four email templates extend via {% extends %}.

The same card design is reused on the /rsvp pages, with the event title
as the card title and {{ app_name }} in the footer, so the user clearly
sees what event they RSVP'd for.

What changes

  • New base: src/backend/core/templates/emails/_invitation_layout.html
    — table-based, inline styles, email-client-safe. Blocks: header_color,
    badge_color, body.
  • Email templates restyled (all reuse existing context vars — no service
    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.
  • /rsvp pages 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 summary
      as the title; error: red header.
    • viewsets_rsvp.py — pass app_name = settings.APP_NAME to the three
      render contexts so the footer reads "via {{ app_name }}".

Translations

  • Added email.invitation.badge (en / fr / nl) — the layout's uppercase
    header label. Previously absent (only update / cancel had badge),
    which would have surfaced the raw key.
  • Added email.reply.badge (en / fr / nl) — same reason.

Summary by CodeRabbit

  • New Features

    • Calendar invitation emails now use a unified template structure for consistent styling across invitations, updates, and cancellations
    • Added badge text translations for calendar invitations and replies in English, French, and Dutch
  • Style

    • RSVP confirmation and response pages redesigned with improved layout and visual styling

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Email and RSVP Template Modernization

Layer / File(s) Summary
Shared Email Layout Foundation
src/backend/core/templates/emails/_invitation_layout.html
New table-based email layout template with inline styles and overridable blocks for header color, badge color, and main content body.
Email Templates Migration to Shared Layout
src/backend/core/templates/emails/calendar_invitation.html, calendar_invitation_cancel.html, calendar_invitation_reply.html, calendar_invitation_update.html
Four email templates refactored from standalone HTML documents to extend the shared layout, reducing duplication and standardizing date/time rendering, location/video sections, and RSVP button styling.
RSVP Shared Styles Partial
src/backend/core/templates/rsvp/_common_styles.html
New CSS template partial with unified wrap/card layout structure, header/body/footer sections, typography, and error-state color variants for RSVP pages.
RSVP UI Template Restyling
src/backend/core/templates/rsvp/confirm.html, response.html
RSVP confirmation and response pages redesigned with wrap/card layout, language defaults changed to English, and integration of shared styling for consistent appearance.
Email Badge Translations
src/frontend/apps/calendars/src/features/i18n/translations.json
Added badge translation keys for email invitations and replies across English, French, and Dutch locales.
RSVP Test Button Color Selectors
src/backend/core/tests/test_rsvp.py
Updated RSVP end-to-end tests to extract action buttons using new color selectors: accept (#435de6), tentative (#626a80), decline (#d2212f).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • suitenumerique/calendars#49: Refactored RSVP invitation email layout and button rendering, directly related to changes in RSVP test suite selector updates for button color extraction.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: refactoring emails and RSVP pages into a card-based layout with design improvements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mosa-riel
Copy link
Copy Markdown
Collaborator Author

Screenshot From 2026-05-28 22-21-03 Screenshot From 2026-05-28 22-20-20

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 817cc41 and 7f420d9.

📒 Files selected for processing (9)
  • src/backend/core/api/viewsets_rsvp.py
  • src/backend/core/templates/emails/_invitation_layout.html
  • src/backend/core/templates/emails/calendar_invitation.html
  • src/backend/core/templates/emails/calendar_invitation_cancel.html
  • src/backend/core/templates/emails/calendar_invitation_reply.html
  • src/backend/core/templates/emails/calendar_invitation_update.html
  • src/backend/core/templates/rsvp/confirm.html
  • src/backend/core/templates/rsvp/response.html
  • src/frontend/apps/calendars/src/features/i18n/translations.json

Comment thread src/backend/core/api/viewsets_rsvp.py Outdated
"error_title": t("rsvp.error.invalidLink", lang),
"header_color": "#dc2626",
"lang": lang,
"app_name": getattr(settings, "APP_NAME", "Calendars"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment thread src/backend/core/templates/emails/calendar_invitation_cancel.html
Comment thread src/backend/core/templates/rsvp/response.html
mosa-riel and others added 5 commits May 28, 2026 22:51
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.
@mosa-riel mosa-riel force-pushed the feat/email-rsvp-restyle branch from 4153cb3 to 3bc5430 Compare May 28, 2026 20:52
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>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7f420d9 and 0930e13.

📒 Files selected for processing (10)
  • src/backend/core/templates/emails/_invitation_layout.html
  • src/backend/core/templates/emails/calendar_invitation.html
  • src/backend/core/templates/emails/calendar_invitation_cancel.html
  • src/backend/core/templates/emails/calendar_invitation_reply.html
  • src/backend/core/templates/emails/calendar_invitation_update.html
  • src/backend/core/templates/rsvp/_common_styles.html
  • src/backend/core/templates/rsvp/confirm.html
  • src/backend/core/templates/rsvp/response.html
  • src/backend/core/tests/test_rsvp.py
  • src/frontend/apps/calendars/src/features/i18n/translations.json

Comment on lines +1 to +8
{% 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
{% 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).

Comment on lines +34 to +36
<div class="footer">
<p>Calendars</p>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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".

Comment on lines +37 to +39
<div class="footer">
<p>Calendars</p>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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".

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.

1 participant