Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions app/backend/src/couchers/email/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

import re
from dataclasses import dataclass
from dataclasses import asdict, dataclass
from functools import lru_cache
from html import unescape
from pathlib import Path
Expand All @@ -25,22 +25,22 @@ class EmailBlock:
pass


@dataclass(kw_only=True)
@dataclass(kw_only=True, slots=True)
class ParaBlock(EmailBlock):
"""A paragraph of text which may contain span-level HTML."""

text: str | Markup


@dataclass(kw_only=True)
@dataclass(kw_only=True, slots=True)
class UserBlock(EmailBlock):
"""A banner with another user's profile information, for example preceding a quoted message."""

info: UserInfo
comment: str | Markup | None


@dataclass(kw_only=True)
@dataclass(kw_only=True, slots=True)
class UserInfo:
name: str
age: int
Expand All @@ -59,22 +59,32 @@ def dummy_bob() -> UserInfo:
)


@dataclass(kw_only=True)
@dataclass(kw_only=True, slots=True)
class QuoteBlock(EmailBlock):
"""A quoted message, typically from another user. Either plaintext or markdown."""

text: str
markdown: bool


@dataclass(kw_only=True)
@dataclass(kw_only=True, slots=True)
class ActionBlock(EmailBlock):
"""An action that can be performed by the user in response to the email."""

text: str
target_url: str


@dataclass(kw_only=True, slots=True)
class TwoButtonHTMLBlock(EmailBlock):
"""An HTML-only block for rendering as side-by-side buttons."""

text_1: str
target_url_1: str
text_2: str
target_url_2: str


class EmailBlocksBuilder:
"""
Builder object for constructing a list of EmailBlock's to form the body of an email.
Expand Down Expand Up @@ -276,6 +286,7 @@ class HTMLRenderer:
user_block_template: Jinja2Template
quote_block_template: Jinja2Template
action_block_template: Jinja2Template
two_buttons_block_template: Jinja2Template

def render(
self,
Expand All @@ -300,10 +311,10 @@ def render(
)

# Render each block
for block in blocks:
for block in type(self)._merge_action_blocks(blocks):
match block:
case ParaBlock():
concats.append(self.para_block_template.render(block.__dict__, loc_context))
concats.append(self.para_block_template.render(asdict(block), loc_context))
case UserBlock():
concats.append(
self.user_block_template.render(
Expand All @@ -321,7 +332,9 @@ def render(
args = {"text": Markup(_markdown.render(block.text)) if block.markdown else block.text}
concats.append(self.quote_block_template.render(args, loc_context))
case ActionBlock():
concats.append(self.action_block_template.render(block.__dict__, loc_context))
concats.append(self.action_block_template.render(asdict(block), loc_context))
case TwoButtonHTMLBlock():
concats.append(self.two_buttons_block_template.render(asdict(block), loc_context))
case _:
raise TypeError(f"Unexpected email block type: {block.__class__}")

Expand All @@ -331,6 +344,28 @@ def render(

return "\n".join(concats)

@staticmethod
def _merge_action_blocks(blocks: list[EmailBlock]) -> list[EmailBlock]:
"""Merge any two subsequent action blocks into a single two-button block."""
blocks = blocks.copy()

block_index = 0
while block_index + 1 < len(blocks):
block = blocks[block_index]
next_block = blocks[block_index + 1]
if isinstance(block, ActionBlock) and isinstance(next_block, ActionBlock):
blocks[block_index] = TwoButtonHTMLBlock(
target_url_1=block.target_url,
text_1=block.text,
target_url_2=next_block.target_url,
text_2=next_block.text,
)
blocks.pop(block_index + 1)

block_index += 1

return blocks

@lru_cache(maxsize=1)
@staticmethod
def default() -> HTMLRenderer:
Expand All @@ -352,13 +387,14 @@ def from_template(template: str) -> HTMLRenderer:
user_block_template=Jinja2Template(source=block_templates["user"], html=True),
quote_block_template=Jinja2Template(source=block_templates["quote"], html=True),
action_block_template=Jinja2Template(source=block_templates["action"], html=True),
two_buttons_block_template=Jinja2Template(source=block_templates["two-buttons"], html=True),
)


# Matches a begin-block / end-block pair of comments in the html file containing template blocks.
_block_regex = re.compile(
r"""
<!-- begin-block:(?P<name>\w+) -->\s*
<!-- begin-block:(?P<name>[\w-]+) -->\s*
(?P<snippet>[\s\S]*?)
\s*<!-- end-block:(?P=name) -->
""".strip(),
Expand Down
15 changes: 15 additions & 0 deletions app/backend/templates/v2/blocks.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@
</mj-section>
<!-- end-block:action -->

<!-- begin-block:two-buttons -->
<mj-section padding="0px">
<mj-column>
<mj-button background-color="#00a398" color="white" border-radius="5px" href="{{ target_url_1 }}">
<b>{{ text_1 }}</b>
</mj-button>
</mj-column>
<mj-column>
<mj-button background-color="#00a398" color="white" border-radius="5px" href="{{ target_url_2 }}">
<b>{{ text_2 }}</b>
</mj-button>
</mj-column>
</mj-section>
<!-- end-block:two-buttons -->

<!-- begin-block:user -->
<mj-section padding="0px" text-align="left">
<mj-column>
Expand Down
70 changes: 69 additions & 1 deletion app/backend/templates/v2/generated_html/blocks.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
width: 100% !important;
max-width: 100%;
}

.mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
}

</style>
Expand All @@ -79,6 +84,11 @@
max-width: 100%;
}

.moz-text-html .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}

</style>
<style type="text/css">
@media only screen and (max-width:479px) {
Expand Down Expand Up @@ -218,7 +228,65 @@
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr><![endif]-->
<!-- end-block:action --><!-- begin-block:user -->
<!-- end-block:action --><!-- begin-block:two-buttons -->
<!--[if mso | IE]><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:560px;" width="560" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:560px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:280px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#00a398" role="presentation" style="border:none;border-radius:5px;cursor:auto;mso-padding-alt:10px 25px;background:#00a398;" valign="middle">
<a href="{{ target_url_1 }}" style="display: inline-block; background: #00a398; color: white; font-family: Ubuntu, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 120%; margin: 0; text-decoration: none; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 5px;" target="_blank">
<b>{{ text_1 }}</b>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:280px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#00a398" role="presentation" style="border:none;border-radius:5px;cursor:auto;mso-padding-alt:10px 25px;background:#00a398;" valign="middle">
<a href="{{ target_url_2 }}" style="display: inline-block; background: #00a398; color: white; font-family: Ubuntu, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 120%; margin: 0; text-decoration: none; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 5px;" target="_blank">
<b>{{ text_2 }}</b>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr><![endif]-->
<!-- end-block:two-buttons --><!-- begin-block:user -->
<!--[if mso | IE]><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:560px;" width="560" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:560px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
Expand Down
Loading