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

1""" 

2Defines data models for each email we sent out to users. 

3""" 

4 

5from abc import ABC, abstractmethod 

6from dataclasses import dataclass 

7from datetime import date 

8from typing import Self 

9 

10from markupsafe import Markup 

11 

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 

23 

24 

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 """ 

31 

32 user_name: str 

33 

34 @abstractmethod 

35 def get_subject_line(self, loc_context: LocalizationContext) -> str: 

36 """Gets the subject line header of the email.""" 

37 ... 

38 

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 

42 

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

47 

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

53 

54 # Helpers for localizing email-specific strings 

55 @property 

56 @abstractmethod 

57 def _localize_key_prefix(self) -> str: ... 

58 

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) 

63 

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) 

70 

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) 

75 

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)) 

80 

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)) 

89 

90 def _quote(self, text: str) -> QuoteBlock: 

91 return QuoteBlock(text=text) 

92 

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) 

97 

98 

99@dataclass 

100class HostRequestReceived(EmailBase): 

101 """Sent to a host to notify them they have received a request from a surfer.""" 

102 

103 surfer: UserInfo 

104 from_date: date 

105 to_date: date 

106 text: str 

107 view_url: str 

108 quick_decline_url: str 

109 

110 def get_subject_line(self, loc_context: LocalizationContext) -> str: 

111 return self._text(loc_context, "subject", {"name": self.surfer.name}) 

112 

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 ] 

130 

131 @property 

132 def _localize_key_prefix(self) -> str: 

133 return "host_request_received" 

134 

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 )