Add models for EmailTemplate, ActivityLog, and NotificationLog

Introduced new database models to handle email templates, activity logs, and notification logs, including relevant enums and utilities. Updated ER diagram to reflect new relationships and attributes, enhancing event tracking and notification management capabilities.
This commit is contained in:
2025-02-27 19:54:26 +01:00
parent 7247190f5f
commit 9d71fc7fcd
5 changed files with 342 additions and 4 deletions

View File

@@ -0,0 +1,120 @@
from enum import Enum
from sqlalchemy import Column, String, ForeignKey, Enum as SQLEnum, Text
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from .base import Base, TimestampMixin, UUIDMixin
class ActivityType(str, Enum):
# Event Activities
EVENT_CREATED = "event_created"
EVENT_UPDATED = "event_updated"
# Guest Activities
GUEST_ADDED = "guest_added"
GUEST_INVITED = "guest_invited"
GUEST_RSVP = "guest_rsvp"
# Gift Activities
GIFT_ADDED = "gift_added"
GIFT_UPDATED = "gift_updated"
GIFT_RESERVED = "gift_reserved"
GIFT_PURCHASED = "gift_purchased"
# User Activities
USER_LOGIN = "user_login"
USER_LOGOUT = "user_logout"
USER_CREATED = "user_created"
# Manager Activities
MANAGER_ADDED = "manager_added"
MANAGER_REMOVED = "manager_removed"
MANAGER_UPDATED = "manager_updated"
# Media Activities
MEDIA_UPLOADED = "media_uploaded"
MEDIA_DELETED = "media_deleted"
# Other
ACCESS_DENIED = "access_denied"
SYSTEM_ERROR = "system_error"
class ActivityLog(Base, UUIDMixin, TimestampMixin):
__tablename__ = 'activity_logs'
# Activity Details
activity_type = Column(SQLEnum(ActivityType), nullable=False)
description = Column(Text)
# Context
event_id = Column(UUID(as_uuid=True), ForeignKey('events.id')) # Optional, for event-specific activities
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id')) # Optional, for user-initiated activities
guest_id = Column(UUID(as_uuid=True), ForeignKey('guests.id')) # Optional, for guest activities
# Additional Context
target_id = Column(UUID(as_uuid=True)) # Generic ID of the target object (gift, media, etc.)
target_type = Column(String) # Type of the target object ("gift", "media", etc.)
# Request Information
ip_address = Column(String)
user_agent = Column(String)
# Additional Data
data = Column(JSONB, default=dict)
# Relationships
event = relationship("Event")
user = relationship("User", foreign_keys=[user_id])
guest = relationship("Guest")
def __repr__(self):
return f"<ActivityLog {self.activity_type.value} event_id={self.event_id}>"
@classmethod
def log_event_activity(cls, activity_type, event_id, user_id=None, description=None,
ip_address=None, user_agent=None, **data):
"""
Helper method to create an event activity log entry
"""
return cls(
activity_type=activity_type,
event_id=event_id,
user_id=user_id,
description=description,
ip_address=ip_address,
user_agent=user_agent,
data=data
)
@classmethod
def log_user_activity(cls, activity_type, user_id, event_id=None, description=None,
ip_address=None, user_agent=None, **data):
"""
Helper method to create a user activity log entry
"""
return cls(
activity_type=activity_type,
user_id=user_id,
event_id=event_id,
description=description,
ip_address=ip_address,
user_agent=user_agent,
data=data
)
@classmethod
def log_guest_activity(cls, activity_type, guest_id, event_id, description=None,
ip_address=None, user_agent=None, **data):
"""
Helper method to create a guest activity log entry
"""
return cls(
activity_type=activity_type,
guest_id=guest_id,
event_id=event_id,
description=description,
ip_address=ip_address,
user_agent=user_agent,
data=data
)

View File

@@ -0,0 +1,70 @@
from enum import Enum
from sqlalchemy import Column, String, Boolean, Enum as SQLEnum, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID, JSONB
from .base import Base, TimestampMixin, UUIDMixin
class TemplateType(str, Enum):
INVITATION = "invitation"
REMINDER = "reminder"
CONFIRMATION = "confirmation"
UPDATE = "update"
THANK_YOU = "thank_you"
CUSTOM = "custom"
class EmailTemplate(Base, UUIDMixin, TimestampMixin):
__tablename__ = 'email_templates'
# Template Identification
name = Column(String, nullable=False)
description = Column(String)
template_type = Column(SQLEnum(TemplateType), nullable=False)
# Template Content
subject = Column(String, nullable=False)
html_content = Column(Text, nullable=False)
text_content = Column(Text, nullable=False)
# Template Variables
variables = Column(JSONB, default=dict)
# Organization
event_id = Column(UUID(as_uuid=True), ForeignKey('events.id')) # Optional, for event-specific templates
created_by = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False)
# Status
is_active = Column(Boolean, default=True, nullable=False)
is_system = Column(Boolean, default=False, nullable=False) # For system-provided templates
# Relationships
event = relationship("Event", back_populates="email_templates", foreign_keys=[event_id])
creator = relationship("User", foreign_keys=[created_by])
# Ensure unique template names within an event or globally for system templates
__table_args__ = (
UniqueConstraint('event_id', 'name', name='uq_event_template_name'),
)
def __repr__(self):
return f"<EmailTemplate {self.name} ({self.template_type.value})>"
def render(self, context: dict) -> tuple[str, str, str]:
"""
Render the template with given context
Returns tuple of (subject, html_content, text_content)
"""
# This would normally use a template engine like Jinja2
# For now, we'll use a simple placeholder implementation
subject = self.subject
html = self.html_content
text = self.text_content
# Replace placeholders in the form {{variable}}
for key, value in context.items():
placeholder = f"{{{{{key}}}}}"
subject = subject.replace(placeholder, str(value))
html = html.replace(placeholder, str(value))
text = text.replace(placeholder, str(value))
return subject, html, text

View File

@@ -1,11 +1,10 @@
# backend/models/event.py
from datetime import datetime, timezone
from sqlalchemy import (
Column, String, DateTime, Time, Boolean, Integer, ForeignKey, JSON,
UniqueConstraint
)
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from .base import Base, TimestampMixin, UUIDMixin
@@ -59,4 +58,4 @@ class Event(Base, UUIDMixin, TimestampMixin):
)
def __repr__(self):
return f"<Event {self.title} ({self.event_date})>"
return f"<Event {self.title} ({self.event_date})>"

View File

@@ -0,0 +1,87 @@
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import Column, String, ForeignKey, Enum as SQLEnum, Text, DateTime
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from .base import Base, TimestampMixin, UUIDMixin
class NotificationType(str, Enum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
IN_APP = "in_app"
class NotificationStatus(str, Enum):
QUEUED = "queued"
SENT = "sent"
DELIVERED = "delivered"
FAILED = "failed"
OPENED = "opened"
CLICKED = "clicked"
class NotificationLog(Base, UUIDMixin, TimestampMixin):
__tablename__ = 'notification_logs'
# Notification Details
notification_type = Column(SQLEnum(NotificationType), nullable=False)
status = Column(SQLEnum(NotificationStatus), nullable=False, default=NotificationStatus.QUEUED)
# Content
subject = Column(String)
content_preview = Column(String(255)) # Short preview of content
template_id = Column(UUID(as_uuid=True), ForeignKey('email_templates.id'))
# Recipient Information
event_id = Column(UUID(as_uuid=True), ForeignKey('events.id'), nullable=False)
guest_id = Column(UUID(as_uuid=True), ForeignKey('guests.id')) # Optional, if sent to a specific guest
recipient = Column(String, nullable=False) # Email address or phone number
# Tracking
sent_at = Column(DateTime(timezone=True))
delivered_at = Column(DateTime(timezone=True))
opened_at = Column(DateTime(timezone=True))
error_message = Column(Text)
retry_count = Column(Integer, default=0)
external_id = Column(String) # ID from external provider (SendGrid, Twilio, etc.)
# Additional Data
metadata = Column(JSONB, default=dict)
# Relationships
event = relationship("Event")
guest = relationship("Guest")
template = relationship("EmailTemplate")
def __repr__(self):
return f"<NotificationLog {self.notification_type.value} to {self.recipient} status={self.status.value}>"
def mark_sent(self):
"""Mark notification as sent"""
self.status = NotificationStatus.SENT
self.sent_at = datetime.now(timezone.utc)
def mark_delivered(self):
"""Mark notification as delivered"""
self.status = NotificationStatus.DELIVERED
self.delivered_at = datetime.now(timezone.utc)
def mark_opened(self):
"""Mark notification as opened"""
self.status = NotificationStatus.OPENED
self.opened_at = datetime.now(timezone.utc)
def mark_failed(self, error: str):
"""Mark notification as failed with error message"""
self.status = NotificationStatus.FAILED
self.error_message = error
self.retry_count += 1
@property
def can_retry(self):
"""Check if notification can be retried (fewer than 3 attempts)"""
return self.status == NotificationStatus.FAILED and self.retry_count < 3

View File

@@ -2,6 +2,8 @@ erDiagram
User ||--o{ Event : "creates"
User ||--o{ EventManager : "is assigned as"
User ||--o{ Guest : "can be linked to"
User ||--o{ ActivityLog : "generates"
User ||--o{ EmailTemplate : "creates"
User {
uuid id PK
string email UK
@@ -37,6 +39,9 @@ erDiagram
Event ||--o{ GiftItem : "contains"
Event ||--o{ EventMedia : "has"
Event ||--o{ GiftCategory : "has"
Event ||--o{ EmailTemplate : "has"
Event ||--o{ NotificationLog : "generates"
Event ||--o{ ActivityLog : "tracks"
Event }o--|| EventTheme : "uses"
Event {
uuid id PK
@@ -70,6 +75,8 @@ erDiagram
Guest ||--o{ RSVP : "submits"
Guest ||--o{ GiftPurchase : "makes"
Guest ||--o{ NotificationLog : "receives"
Guest ||--o{ ActivityLog : "generates"
Guest {
uuid id PK
uuid event_id FK
@@ -195,5 +202,60 @@ erDiagram
datetime updated_at
}
EmailTemplate {
uuid id PK
string name
string description
enum template_type "INVITATION/REMINDER/CONFIRMATION/UPDATE/THANK_YOU/CUSTOM"
string subject
text html_content
text text_content
json variables
uuid event_id FK "nullable"
uuid created_by FK
boolean is_active
boolean is_system
datetime created_at
datetime updated_at
}
NotificationLog {
uuid id PK
enum notification_type "EMAIL/SMS/PUSH/IN_APP"
enum status "QUEUED/SENT/DELIVERED/FAILED/OPENED/CLICKED"
string subject
string content_preview
uuid template_id FK
uuid event_id FK
uuid guest_id FK "nullable"
string recipient
datetime sent_at
datetime delivered_at
datetime opened_at
string error_message
integer retry_count
string external_id
json metadata
datetime created_at
datetime updated_at
}
ActivityLog {
uuid id PK
enum activity_type
text description
uuid event_id FK "nullable"
uuid user_id FK "nullable"
uuid guest_id FK "nullable"
uuid target_id "generic ID"
string target_type "type of target"
string ip_address
string user_agent
json data
datetime created_at
datetime updated_at
}
EmailTemplate }o--|| NotificationLog : "used for"
User ||--o{ EventManager : "has roles"
Event ||--o{ RSVP : "receives"