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
« prev ^ index » next coverage.py v7.6.10, created at 2025-06-01 15:07 +0000
1"""
2template mailer/push notification formatter v2
3"""
5import logging
6from datetime import date
7from html import escape
8from pathlib import Path
9from zoneinfo import ZoneInfo
11import phonenumbers
12from jinja2 import Environment, FileSystemLoader
13from markdown_it import MarkdownIt
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
20logger = logging.getLogger(__name__)
22template_folder = Path(__file__).parent / ".." / ".." / ".." / "templates" / "v2"
24loader = FileSystemLoader(template_folder)
25env = Environment(loader=loader, trim_blocks=True)
27md = MarkdownIt("zero", {"typographer": True}).enable(["smartquotes", "heading", "hr", "list", "link", "emphasis"])
30def v2esc(value):
31 return escape(str(value))
34def v2multiline(value):
35 return "<br />".join(value.splitlines())
38def v2sf(value):
39 return value
42def v2url(value):
43 return value
46def v2phone(value):
47 return phonenumbers.format_number(phonenumbers.parse(value), phonenumbers.PhoneNumberFormat.INTERNATIONAL)
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")
57def v2time(value, user):
58 tz = ZoneInfo(user.timezone or "Etc/UTC")
59 return value.astimezone(tz=tz).strftime("%-I:%M %p (%H:%M)")
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)")
67def v2avatar(user):
68 if not user.avatar_thumbnail_url:
69 return urls.icon_url()
70 return user.avatar_thumbnail_url
73def v2quote(value):
74 """
75 Multiline quote, use in place of markdown in plaintext emails
76 """
77 return "\n> ".join([""] + value.splitlines())
80def v2markdown(value):
81 return md.render(value)
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
98add_filters(env)
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
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")
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."
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)
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 )