Coverage for app / backend / src / couchers / email / emails.py: 0%
50 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-24 17:34 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-24 17:34 +0000
1"""
2Defines data models for each email we sent out to users.
3"""
5from abc import ABC, abstractmethod
6from dataclasses import dataclass
7from datetime import date
8from typing import Self
10from markupsafe import Markup
12from couchers.email.rendering import (
13 ActionBlock,
14 EmailBlock,
15 ParaBlock,
16 QuoteBlock,
17 UserBlock,
18 UserInfo,
19 get_emails_i18next,
20)
21from couchers.i18n import LocalizationContext
22from couchers.i18n.i18next import SubstitutionDict
25@dataclass
26class EmailBase(ABC):
27 """
28 Base class for email data models, which capture all the data required to render
29 an email's subject line and body as HTML or plaintext, in any locale.
30 """
32 user_name: str
34 @abstractmethod
35 def get_subject_line(self, loc_context: LocalizationContext) -> str:
36 """Gets the subject line header of the email."""
37 ...
39 def get_preview_line(self, loc_context: LocalizationContext) -> str | None:
40 """Gets the line that gets shown as a preview next to the title in users' inboxes."""
41 return None
43 @abstractmethod
44 def get_body_blocks(self, loc_context: LocalizationContext) -> list[EmailBlock]:
45 """Gets the blocks that form the body of the email."""
46 ...
48 @classmethod
49 @abstractmethod
50 def dummy_data(cls) -> Self:
51 """Returns an instance filled with dummy data that can be used for testing."""
52 ...
54 # Helpers for localizing email-specific strings
55 @property
56 @abstractmethod
57 def _localize_key_prefix(self) -> str: ...
59 def _text(self, loc_context: LocalizationContext, key: str, substitutions: SubstitutionDict | None = None) -> str:
60 if "." not in key:
61 key = f"{self._localize_key_prefix}.{key}"
62 return get_emails_i18next().localize(key, loc_context.locale, substitutions)
64 def _markup(
65 self, loc_context: LocalizationContext, key: str, substitutions: SubstitutionDict | None = None
66 ) -> Markup:
67 if "." not in key:
68 key = f"{self._localize_key_prefix}.{key}"
69 return get_emails_i18next().localize_with_markup(key, loc_context.locale, substitutions)
71 # Helpers for creating common blocks
72 def _greeting_line(self, loc_context: LocalizationContext) -> ParaBlock:
73 line = get_emails_i18next().localize("generic.greeting_line", loc_context.locale, {"name": self.user_name})
74 return ParaBlock(text=line)
76 def _para(
77 self, loc_context: LocalizationContext, key: str, substitutions: SubstitutionDict | None = None
78 ) -> ParaBlock:
79 return ParaBlock(text=self._markup(loc_context, key, substitutions))
81 def _user(
82 self,
83 info: UserInfo,
84 loc_context: LocalizationContext,
85 comment_key: str,
86 substitutions: SubstitutionDict | None = None,
87 ) -> UserBlock:
88 return UserBlock(info=info, comment=self._markup(loc_context, comment_key, substitutions))
90 def _quote(self, text: str) -> QuoteBlock:
91 return QuoteBlock(text=text)
93 def _action(
94 self, url: str, loc_context: LocalizationContext, text_key: str, substitutions: SubstitutionDict | None = None
95 ) -> ActionBlock:
96 return ActionBlock(text=self._text(loc_context, text_key, substitutions), target_url=url)
99@dataclass
100class HostRequestReceived(EmailBase):
101 """Sent to a host to notify them they have received a request from a surfer."""
103 surfer: UserInfo
104 from_date: date
105 to_date: date
106 text: str
107 view_url: str
108 quick_decline_url: str
110 def get_subject_line(self, loc_context: LocalizationContext) -> str:
111 return self._text(loc_context, "subject", {"name": self.surfer.name})
113 def get_body_blocks(self, loc_context: LocalizationContext) -> list[EmailBlock]:
114 return [
115 self._greeting_line(loc_context),
116 self._para(loc_context, "event_description", {"name": self.surfer.name}),
117 self._user(
118 self.surfer,
119 loc_context,
120 "requested_dates",
121 {
122 "from_date": loc_context.localize_date(self.from_date),
123 "to_date": loc_context.localize_date(self.to_date),
124 },
125 ),
126 self._quote(self.text),
127 self._action(self.view_url, loc_context, "view_request_link"),
128 self._action(self.quick_decline_url, loc_context, "quick_decline_link"),
129 ]
131 @property
132 def _localize_key_prefix(self) -> str:
133 return "host_request_received"
135 @classmethod
136 def dummy_data(cls) -> HostRequestReceived:
137 return HostRequestReceived(
138 user_name="Alice",
139 surfer=UserInfo(
140 name="Bob",
141 age=42,
142 city="Tokyo",
143 avatar_url="https://example.com/users/bob/avatar.jpg",
144 profile_url="https://example.com/users/bob",
145 ),
146 from_date=date(2000, 1, 1),
147 to_date=date(2000, 1, 2),
148 text="Hello world!",
149 view_url="https://example.com/requests",
150 quick_decline_url="https://example.com/quick-decline",
151 )