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

1"""Email templates and rendering for admin notifications.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from typing import Any 

7 

8from lexigram.admin.services.notifications.models import NotificationType 

9from lexigram.di.decorators import inject 

10 

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

52 

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} 

208 

209 

210@inject 

211class TemplateRenderer: 

212 """Handles rendering of email templates.""" 

213 

214 def __init__(self, app_name: str = "Admin"): 

215 """Initialize with app name for templates.""" 

216 self.app_name = app_name 

217 

218 def render_template( 

219 self, 

220 notification_type: NotificationType, 

221 data: dict[str, Any], 

222 ) -> tuple[str, str, str]: 

223 """Render email template. 

224 

225 Args: 

226 notification_type: Type of notification 

227 data: Template data 

228 

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 ) 

239 

240 subject_tpl, body_tpl = templates 

241 

242 # Add app name to data 

243 full_data = { 

244 "app_name": self.app_name, 

245 **data, 

246 } 

247 

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 ) 

256 

257 # Simple text conversion 

258 text_body = re.sub(r"<[^>]+>", "", body_content) 

259 text_body = re.sub(r"\s+", " ", text_body).strip() 

260 

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 ) 

268 

269 

270__all__ = [ 

271 "ADMIN_EMAIL_BASE", 

272 "EMAIL_TEMPLATES", 

273 "TemplateRenderer", 

274]