Coverage for src/lexigram/admin/services/notifications/templates.py: 89%
27 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Email templates and rendering for admin notifications."""
3from __future__ import annotations
5import re
6from typing import Any
8from lexigram.admin.services.notifications.models import NotificationType
9from lexigram.di.decorators import inject
11# Base template with admin branding
12ADMIN_EMAIL_BASE = """
13<!DOCTYPE html>
14<html>
15<head>
16 <meta charset="utf-8">
17 <meta name="viewport" content="width=device-width, initial-scale=1.0">
18 <style>
19 body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }}
20 .container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
21 .header {{ background: #1a56db; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
22 .content {{ background: #fff; padding: 30px; border: 1px solid #e5e7eb; }}
23 .footer {{ background: #f9fafb; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; border-radius: 0 0 8px 8px; }}
24 .btn {{ display: inline-block; padding: 12px 24px; background: #1a56db; color: white; text-decoration: none; border-radius: 6px; margin: 16px 0; }}
25 .alert {{ padding: 16px; border-radius: 6px; margin: 16px 0; }}
26 .alert-success {{ background: #d1fae5; border: 1px solid #34d399; }}
27 .alert-warning {{ background: #fef3c7; border: 1px solid #f59e0b; }}
28 .alert-error {{ background: #fee2e2; border: 1px solid #ef4444; }}
29 .progress {{ background: #e5e7eb; border-radius: 9999px; height: 8px; overflow: hidden; }}
30 .progress-bar {{ background: #1a56db; height: 100%; transition: width 0.3s; }}
31 table {{ width: 100%; border-collapse: collapse; margin: 16px 0; }}
32 th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }}
33 th {{ background: #f9fafb; font-weight: 600; }}
34 </style>
35</head>
36<body>
37 <div class="container">
38 <div class="header">
39 <h1>{app_name} Admin</h1>
40 </div>
41 <div class="content">
42 {content}
43 </div>
44 <div class="footer">
45 <p>This is an automated notification from {app_name} Admin Panel.</p>
46 <p>You received this email because you are an administrator.</p>
47 </div>
48 </div>
49</body>
50</html>
51"""
53# Specific templates
54EMAIL_TEMPLATES: dict[NotificationType, tuple[str, str]] = {
55 # (subject_template, body_template)
56 NotificationType.USER_CREATED: (
57 "New user created: {user_name}",
58 """
59 <h2>New User Created</h2>
60 <p>A new user account has been created:</p>
61 <table>
62 <tr><th>Name</th><td>{user_name}</td></tr>
63 <tr><th>Email</th><td>{user_email}</td></tr>
64 <tr><th>Role</th><td>{user_role}</td></tr>
65 <tr><th>Created By</th><td>{created_by}</td></tr>
66 <tr><th>Created At</th><td>{created_at}</td></tr>
67 </table>
68 <a href="{user_url}" class="btn">View User</a>
69 """,
70 ),
71 NotificationType.USER_INVITED: (
72 "You've been invited to {app_name} Admin",
73 """
74 <h2>Welcome to {app_name} Admin</h2>
75 <p>Hello {user_name},</p>
76 <p>You've been invited to join {app_name} as an administrator.</p>
77 <p>Click the button below to set up your account:</p>
78 <a href="{invite_url}" class="btn">Accept Invitation</a>
79 <p><small>This invitation expires in {expires_in}.</small></p>
80 """,
81 ),
82 NotificationType.PASSWORD_RESET: (
83 "Password Reset Request",
84 """
85 <h2>Password Reset</h2>
86 <p>Hello {user_name},</p>
87 <p>We received a request to reset your password. Click the button below to create a new password:</p>
88 <a href="{reset_url}" class="btn">Reset Password</a>
89 <p><small>This link expires in {expires_in}. If you didn't request this, you can safely ignore this email.</small></p>
90 """,
91 ),
92 NotificationType.EMAIL_VERIFICATION: (
93 "Verify your email address",
94 """
95 <h2>Verify Your Email</h2>
96 <p>Hello {user_name},</p>
97 <p>Please verify your email address to finish securing your admin account.</p>
98 <a href="{verify_url}" class="btn">Verify Email</a>
99 <p><small>This link expires in {expires_in}. If you didn't request this, you can safely ignore this email.</small></p>
100 """,
101 ),
102 NotificationType.EMAIL_OTP: (
103 "Your verification code",
104 """
105 <h2>Verification Code</h2>
106 <p>Hello {user_name},</p>
107 <p>Your one-time verification code is:</p>
108 <p class="code">{code}</p>
109 <p><small>This code expires in {expires_in}. If you didn't request this, you can safely ignore this email.</small></p>
110 """,
111 ),
112 NotificationType.BULK_STARTED: (
113 "Bulk operation started: {operation_name}",
114 """
115 <h2>Bulk Operation Started</h2>
116 <p>A bulk operation has been started:</p>
117 <table>
118 <tr><th>Operation</th><td>{operation_name}</td></tr>
119 <tr><th>Resource</th><td>{resource}</td></tr>
120 <tr><th>Total Items</th><td>{total_items}</td></tr>
121 <tr><th>Started By</th><td>{started_by}</td></tr>
122 <tr><th>Started At</th><td>{started_at}</td></tr>
123 </table>
124 <p>You will receive another notification when the operation completes.</p>
125 """,
126 ),
127 NotificationType.BULK_PROGRESS: (
128 "Bulk operation progress: {progress}%",
129 """
130 <h2>Bulk Operation Progress</h2>
131 <p>Operation: {operation_name}</p>
132 <div class="progress">
133 <div class="progress-bar" style="width: {progress}%"></div>
134 </div>
135 <p>{processed} of {total_items} items processed ({progress}%)</p>
136 <table>
137 <tr><th>Successful</th><td>{successful}</td></tr>
138 <tr><th>Failed</th><td>{failed}</td></tr>
139 <tr><th>Remaining</th><td>{remaining}</td></tr>
140 </table>
141 """,
142 ),
143 NotificationType.BULK_COMPLETED: (
144 "Bulk operation completed: {operation_name}",
145 """
146 <h2>Bulk Operation Completed</h2>
147 <div class="alert alert-success">
148 <strong>Success!</strong> The bulk operation has completed.
149 </div>
150 <table>
151 <tr><th>Operation</th><td>{operation_name}</td></tr>
152 <tr><th>Resource</th><td>{resource}</td></tr>
153 <tr><th>Total Items</th><td>{total_items}</td></tr>
154 <tr><th>Successful</th><td>{successful}</td></tr>
155 <tr><th>Failed</th><td>{failed}</td></tr>
156 <tr><th>Duration</th><td>{duration}</td></tr>
157 </table>
158 <a href="{results_url}" class="btn">View Results</a>
159 """,
160 ),
161 NotificationType.BULK_FAILED: (
162 "Bulk operation failed: {operation_name}",
163 """
164 <h2>Bulk Operation Failed</h2>
165 <div class="alert alert-error">
166 <strong>Error!</strong> The bulk operation failed.
167 </div>
168 <table>
169 <tr><th>Operation</th><td>{operation_name}</td></tr>
170 <tr><th>Error</th><td>{error_message}</td></tr>
171 <tr><th>Processed Before Failure</th><td>{processed}</td></tr>
172 </table>
173 <p>Please check the logs for more details.</p>
174 """,
175 ),
176 NotificationType.EXPORT_READY: (
177 "Your export is ready: {export_name}",
178 """
179 <h2>Export Ready</h2>
180 <div class="alert alert-success">
181 <strong>Success!</strong> Your data export is ready for download.
182 </div>
183 <table>
184 <tr><th>Export</th><td>{export_name}</td></tr>
185 <tr><th>Format</th><td>{format}</td></tr>
186 <tr><th>Records</th><td>{record_count}</td></tr>
187 <tr><th>File Size</th><td>{file_size}</td></tr>
188 </table>
189 <a href="{download_url}" class="btn">Download Export</a>
190 <p><small>This link expires in {expires_in}.</small></p>
191 """,
192 ),
193 NotificationType.SYSTEM_ALERT: (
194 "[{severity}] System Alert: {alert_title}",
195 """
196 <h2>System Alert</h2>
197 <div class="alert alert-{severity_class}">
198 <strong>{severity}:</strong> {alert_title}
199 </div>
200 <p>{alert_message}</p>
201 <table>
202 <tr><th>Time</th><td>{occurred_at}</td></tr>
203 <tr><th>Component</th><td>{component}</td></tr>
204 </table>
205 """,
206 ),
207}
210@inject
211class TemplateRenderer:
212 """Handles rendering of email templates."""
214 def __init__(self, app_name: str = "Admin"):
215 """Initialize with app name for templates."""
216 self.app_name = app_name
218 def render_template(
219 self,
220 notification_type: NotificationType,
221 data: dict[str, Any],
222 ) -> tuple[str, str, str]:
223 """Render email template.
225 Args:
226 notification_type: Type of notification
227 data: Template data
229 Returns:
230 Tuple of (subject, text_body, html_body)
231 """
232 templates = EMAIL_TEMPLATES.get(notification_type)
233 if not templates:
234 return (
235 data.get("subject", "Admin Notification"),
236 data.get("body", ""),
237 "",
238 )
240 subject_tpl, body_tpl = templates
242 # Add app name to data
243 full_data = {
244 "app_name": self.app_name,
245 **data,
246 }
248 # Render
249 try:
250 subject = subject_tpl.format(**full_data)
251 body_content = body_tpl.format(**full_data)
252 html_body = ADMIN_EMAIL_BASE.format(
253 app_name=self.app_name,
254 content=body_content,
255 )
257 # Simple text conversion
258 text_body = re.sub(r"<[^>]+>", "", body_content)
259 text_body = re.sub(r"\s+", " ", text_body).strip()
261 return subject, text_body, html_body
262 except KeyError as e:
263 return (
264 f"Admin Notification: {notification_type.value}",
265 f"Missing template data: {e}",
266 "",
267 )
270__all__ = [
271 "ADMIN_EMAIL_BASE",
272 "EMAIL_TEMPLATES",
273 "TemplateRenderer",
274]