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:
120
backend/app/models/activity_log.py
Normal file
120
backend/app/models/activity_log.py
Normal 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
|
||||||
|
)
|
||||||
70
backend/app/models/email_template.py
Normal file
70
backend/app/models/email_template.py
Normal 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
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
# backend/models/event.py
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Column, String, DateTime, Time, Boolean, Integer, ForeignKey, JSON,
|
Column, String, DateTime, Time, Boolean, Integer, ForeignKey, JSON,
|
||||||
UniqueConstraint
|
UniqueConstraint
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from .base import Base, TimestampMixin, UUIDMixin
|
from .base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -59,4 +58,4 @@ class Event(Base, UUIDMixin, TimestampMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Event {self.title} ({self.event_date})>"
|
return f"<Event {self.title} ({self.event_date})>"
|
||||||
|
|||||||
87
backend/app/models/notification_log.py
Normal file
87
backend/app/models/notification_log.py
Normal 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
|
||||||
@@ -2,6 +2,8 @@ erDiagram
|
|||||||
User ||--o{ Event : "creates"
|
User ||--o{ Event : "creates"
|
||||||
User ||--o{ EventManager : "is assigned as"
|
User ||--o{ EventManager : "is assigned as"
|
||||||
User ||--o{ Guest : "can be linked to"
|
User ||--o{ Guest : "can be linked to"
|
||||||
|
User ||--o{ ActivityLog : "generates"
|
||||||
|
User ||--o{ EmailTemplate : "creates"
|
||||||
User {
|
User {
|
||||||
uuid id PK
|
uuid id PK
|
||||||
string email UK
|
string email UK
|
||||||
@@ -37,6 +39,9 @@ erDiagram
|
|||||||
Event ||--o{ GiftItem : "contains"
|
Event ||--o{ GiftItem : "contains"
|
||||||
Event ||--o{ EventMedia : "has"
|
Event ||--o{ EventMedia : "has"
|
||||||
Event ||--o{ GiftCategory : "has"
|
Event ||--o{ GiftCategory : "has"
|
||||||
|
Event ||--o{ EmailTemplate : "has"
|
||||||
|
Event ||--o{ NotificationLog : "generates"
|
||||||
|
Event ||--o{ ActivityLog : "tracks"
|
||||||
Event }o--|| EventTheme : "uses"
|
Event }o--|| EventTheme : "uses"
|
||||||
Event {
|
Event {
|
||||||
uuid id PK
|
uuid id PK
|
||||||
@@ -70,6 +75,8 @@ erDiagram
|
|||||||
|
|
||||||
Guest ||--o{ RSVP : "submits"
|
Guest ||--o{ RSVP : "submits"
|
||||||
Guest ||--o{ GiftPurchase : "makes"
|
Guest ||--o{ GiftPurchase : "makes"
|
||||||
|
Guest ||--o{ NotificationLog : "receives"
|
||||||
|
Guest ||--o{ ActivityLog : "generates"
|
||||||
Guest {
|
Guest {
|
||||||
uuid id PK
|
uuid id PK
|
||||||
uuid event_id FK
|
uuid event_id FK
|
||||||
@@ -195,5 +202,60 @@ erDiagram
|
|||||||
datetime updated_at
|
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"
|
User ||--o{ EventManager : "has roles"
|
||||||
Event ||--o{ RSVP : "receives"
|
Event ||--o{ RSVP : "receives"
|
||||||
|
|||||||
Reference in New Issue
Block a user