Coverage for src/couchers/templates/v2.py: 99%

68 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-06-01 15:07 +0000

1""" 

2template mailer/push notification formatter v2 

3""" 

4 

5import logging 

6from datetime import date 

7from html import escape 

8from pathlib import Path 

9from zoneinfo import ZoneInfo 

10 

11import phonenumbers 

12from jinja2 import Environment, FileSystemLoader 

13from markdown_it import MarkdownIt 

14 

15from couchers import urls 

16from couchers.config import config 

17from couchers.email import queue_email 

18from couchers.utils import get_tz_as_text, now, to_aware_datetime 

19 

20logger = logging.getLogger(__name__) 

21 

22template_folder = Path(__file__).parent / ".." / ".." / ".." / "templates" / "v2" 

23 

24loader = FileSystemLoader(template_folder) 

25env = Environment(loader=loader, trim_blocks=True) 

26 

27md = MarkdownIt("zero", {"typographer": True}).enable(["smartquotes", "heading", "hr", "list", "link", "emphasis"]) 

28 

29 

30def v2esc(value): 

31 return escape(str(value)) 

32 

33 

34def v2multiline(value): 

35 return "<br />".join(value.splitlines()) 

36 

37 

38def v2sf(value): 

39 return value 

40 

41 

42def v2url(value): 

43 return value 

44 

45 

46def v2phone(value): 

47 return phonenumbers.format_number(phonenumbers.parse(value), phonenumbers.PhoneNumberFormat.INTERNATIONAL) 

48 

49 

50def v2date(value, user): 

51 # todo: user locale-based date formatting 

52 if isinstance(value, str): 

53 value = date.fromisoformat(value) 

54 return value.strftime("%A %-d %B %Y") 

55 

56 

57def v2time(value, user): 

58 tz = ZoneInfo(user.timezone or "Etc/UTC") 

59 return value.astimezone(tz=tz).strftime("%-I:%M %p (%H:%M)") 

60 

61 

62def v2timestamp(value, user): 

63 tz = ZoneInfo(user.timezone or "Etc/UTC") 

64 return to_aware_datetime(value).astimezone(tz=tz).strftime("%A %-d %B %Y at %-I:%M %p (%H:%M)") 

65 

66 

67def v2avatar(user): 

68 if not user.avatar_thumbnail_url: 

69 return urls.icon_url() 

70 return user.avatar_thumbnail_url 

71 

72 

73def v2quote(value): 

74 """ 

75 Multiline quote, use in place of markdown in plaintext emails 

76 """ 

77 return "\n> ".join([""] + value.splitlines()) 

78 

79 

80def v2markdown(value): 

81 return md.render(value) 

82 

83 

84def add_filters(env): 

85 env.filters["v2esc"] = v2esc 

86 env.filters["v2multiline"] = v2multiline 

87 env.filters["v2sf"] = v2sf 

88 env.filters["v2url"] = v2url 

89 env.filters["v2phone"] = v2phone 

90 env.filters["v2date"] = v2date 

91 env.filters["v2time"] = v2time 

92 env.filters["v2timestamp"] = v2timestamp 

93 env.filters["v2avatar"] = v2avatar 

94 env.filters["v2quote"] = v2quote 

95 env.filters["v2markdown"] = v2markdown 

96 

97 

98add_filters(env) 

99 

100 

101def send_simple_pretty_email(session, recipient, subject, template_name, template_args): 

102 """ 

103 This is a simplified version of couchers.notifications.background._send_email_notification 

104 

105 It's for the few security emails where we don't have a user to email but send directly to an email address. 

106 """ 

107 template_args["_year"] = now().year 

108 template_args["_timezone_display"] = get_tz_as_text("Etc/UTC") 

109 

110 plain_unsub_section = "\n\n---\n\nThis is a security email, you cannot unsubscribe from it." 

111 html_unsub_section = "This is a security email, you cannot unsubscribe from it." 

112 

113 plain_tmplt = (template_folder / f"{template_name}.txt").read_text() 

114 plain = env.from_string(plain_tmplt + plain_unsub_section).render(template_args) 

115 html_tmplt = (template_folder / "generated_html" / f"{template_name}.html").read_text() 

116 html = env.from_string(html_tmplt.replace("___UNSUB_SECTION___", html_unsub_section)).render(template_args) 

117 

118 queue_email( 

119 session, 

120 sender_name=config["NOTIFICATION_EMAIL_SENDER"], 

121 sender_email=config["NOTIFICATION_EMAIL_ADDRESS"], 

122 recipient=recipient, 

123 subject=config["NOTIFICATION_PREFIX"] + subject, 

124 plain=plain, 

125 html=html, 

126 source_data=config["VERSION"] + f"/{template_name}", 

127 )