notification_templates.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. """API routes for notification template management."""
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy import select
  4. from sqlalchemy.ext.asyncio import AsyncSession
  5. from backend.app.core.auth import RequirePermissionIfAuthEnabled
  6. from backend.app.core.database import get_db
  7. from backend.app.core.permissions import Permission
  8. from backend.app.models.notification_template import DEFAULT_TEMPLATES, NotificationTemplate
  9. from backend.app.models.user import User
  10. from backend.app.schemas.notification_template import (
  11. EVENT_VARIABLES,
  12. PHOTO_CAPABLE_EVENTS,
  13. SAMPLE_DATA,
  14. EventVariablesResponse,
  15. NotificationTemplateResponse,
  16. NotificationTemplateUpdate,
  17. TemplatePreviewRequest,
  18. TemplatePreviewResponse,
  19. )
  20. from backend.app.services.notification_service import notification_service
  21. router = APIRouter(prefix="/notification-templates", tags=["notification-templates"])
  22. # Event type display names
  23. EVENT_NAMES = {
  24. "print_start": "Print Started",
  25. "print_complete": "Print Completed",
  26. "print_failed": "Print Failed",
  27. "print_stopped": "Print Stopped",
  28. "print_progress": "Print Progress",
  29. "billing_charge_failed": "Billing Charge Failed",
  30. "printer_offline": "Printer Offline",
  31. "printer_error": "Printer Error",
  32. "ai_failure_detection": "AI Failure Detection",
  33. "plate_not_empty": "Plate Not Empty",
  34. "plate_clear_required": "Plate Clear Required",
  35. "print_confirm_request": "Print Outcome Confirmation",
  36. "print_missing_spool_assignment": "Missing Spool Assignment",
  37. "bed_cooled": "Bed Cooled",
  38. "ams_humidity_high": "AMS Humidity High",
  39. "ams_temperature_high": "AMS Temperature High",
  40. "ams_drying_suspended": "Auto-Drying Suspended",
  41. "ha_sensor_alert": "Printer Sensor Alert",
  42. "location_ha_sensor_alert": "Storage Location Sensor Alert",
  43. "filament_low": "Filament Low",
  44. "maintenance_due": "Maintenance Due",
  45. "first_layer_complete": "First Layer Complete",
  46. "print_pause_upcoming": "Pause Coming Up",
  47. "test": "Test Notification",
  48. # Queue notifications
  49. "queue_job_added": "Queue Job Added",
  50. "queue_job_assigned": "Queue Job Assigned",
  51. "queue_job_started": "Queue Job Started",
  52. "queue_job_waiting": "Queue Job Waiting",
  53. "queue_job_skipped": "Queue Job Skipped",
  54. "queue_job_failed": "Queue Job Failed",
  55. "queue_completed": "Queue Completed",
  56. # Inventory stock alerts
  57. "stock_reorder_alert": "Stock Reorder Alert",
  58. "stock_break_alert": "Stock Break Alert",
  59. # User management
  60. "user_created": "Welcome Email",
  61. "password_reset": "Password Reset",
  62. # User email print notifications
  63. "user_print_start": "User Print Started Email",
  64. "user_print_complete": "User Print Completed Email",
  65. "user_print_failed": "User Print Failed Email",
  66. "user_print_stopped": "User Print Stopped Email",
  67. }
  68. @router.get("", response_model=list[NotificationTemplateResponse])
  69. @router.get("/", response_model=list[NotificationTemplateResponse])
  70. async def get_templates(
  71. db: AsyncSession = Depends(get_db, scope="function"),
  72. _: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATION_TEMPLATES_READ),
  73. ):
  74. """Get all notification templates."""
  75. result = await db.execute(select(NotificationTemplate).order_by(NotificationTemplate.id))
  76. return result.scalars().all()
  77. @router.get("/variables", response_model=list[EventVariablesResponse])
  78. async def get_variables(
  79. _: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATION_TEMPLATES_READ),
  80. ):
  81. """Get available variables for each event type."""
  82. return [
  83. EventVariablesResponse(
  84. event_type=event_type,
  85. event_name=EVENT_NAMES.get(event_type, event_type),
  86. variables=variables,
  87. supports_photo=event_type in PHOTO_CAPABLE_EVENTS,
  88. )
  89. for event_type, variables in EVENT_VARIABLES.items()
  90. ]
  91. @router.get("/{template_id}", response_model=NotificationTemplateResponse)
  92. async def get_template(
  93. template_id: int,
  94. db: AsyncSession = Depends(get_db, scope="function"),
  95. _: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATION_TEMPLATES_READ),
  96. ):
  97. """Get a single notification template."""
  98. result = await db.execute(select(NotificationTemplate).where(NotificationTemplate.id == template_id))
  99. template = result.scalar_one_or_none()
  100. if not template:
  101. raise HTTPException(status_code=404, detail="Template not found")
  102. return template
  103. @router.put("/{template_id}", response_model=NotificationTemplateResponse)
  104. async def update_template(
  105. template_id: int,
  106. update: NotificationTemplateUpdate,
  107. db: AsyncSession = Depends(get_db, scope="function"),
  108. _: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATION_TEMPLATES_UPDATE),
  109. ):
  110. """Update a notification template."""
  111. result = await db.execute(select(NotificationTemplate).where(NotificationTemplate.id == template_id))
  112. template = result.scalar_one_or_none()
  113. if not template:
  114. raise HTTPException(status_code=404, detail="Template not found")
  115. if update.title_template is not None:
  116. template.title_template = update.title_template
  117. if update.body_template is not None:
  118. template.body_template = update.body_template
  119. await db.commit()
  120. await db.refresh(template)
  121. # Clear template cache so changes take effect immediately
  122. notification_service.clear_template_cache()
  123. return template
  124. @router.post("/{template_id}/reset", response_model=NotificationTemplateResponse)
  125. async def reset_template(
  126. template_id: int,
  127. db: AsyncSession = Depends(get_db, scope="function"),
  128. _: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATION_TEMPLATES_UPDATE),
  129. ):
  130. """Reset a notification template to its default values."""
  131. result = await db.execute(select(NotificationTemplate).where(NotificationTemplate.id == template_id))
  132. template = result.scalar_one_or_none()
  133. if not template:
  134. raise HTTPException(status_code=404, detail="Template not found")
  135. # Find the default template
  136. default = next(
  137. (t for t in DEFAULT_TEMPLATES if t["event_type"] == template.event_type),
  138. None,
  139. )
  140. if not default:
  141. raise HTTPException(status_code=500, detail="Default template not found")
  142. template.title_template = default["title_template"]
  143. template.body_template = default["body_template"]
  144. await db.commit()
  145. await db.refresh(template)
  146. # Clear template cache so changes take effect immediately
  147. notification_service.clear_template_cache()
  148. return template
  149. @router.post("/preview", response_model=TemplatePreviewResponse)
  150. async def preview_template(
  151. request: TemplatePreviewRequest,
  152. _: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATION_TEMPLATES_READ),
  153. ):
  154. """Preview a template with sample data."""
  155. sample = SAMPLE_DATA.get(request.event_type, {})
  156. # Safe template rendering - replace missing vars with empty string
  157. def safe_format(template: str, data: dict) -> str:
  158. result = template
  159. for key, value in data.items():
  160. result = result.replace("{" + key + "}", str(value))
  161. # Remove any remaining unreplaced placeholders
  162. import re
  163. result = re.sub(r"\{[a-z_]+\}", "", result)
  164. return result
  165. return TemplatePreviewResponse(
  166. title=safe_format(request.title_template, sample),
  167. body=safe_format(request.body_template, sample),
  168. )