mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 04:11:08 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
772 lines
26 KiB
Python
772 lines
26 KiB
Python
from django.dispatch import receiver
|
|
from django.db.models.signals import post_save
|
|
from django.contrib.auth.models import AbstractUser
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
from django.db import models
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _
|
|
import secrets
|
|
from datetime import timedelta
|
|
from django.utils import timezone
|
|
from apps.core.history import TrackedModel
|
|
from apps.core.choices import RichChoiceField
|
|
import pghistory
|
|
|
|
|
|
def generate_random_id(model_class, id_field):
|
|
"""Generate a random ID starting at 4 digits, expanding to 5 if needed"""
|
|
while True:
|
|
# Try to get a 4-digit number first
|
|
new_id = str(secrets.SystemRandom().randint(1000, 9999))
|
|
if not model_class.objects.filter(**{id_field: new_id}).exists():
|
|
return new_id
|
|
|
|
# If all 4-digit numbers are taken, try 5 digits
|
|
new_id = str(secrets.SystemRandom().randint(10000, 99999))
|
|
if not model_class.objects.filter(**{id_field: new_id}).exists():
|
|
return new_id
|
|
|
|
|
|
@pghistory.track()
|
|
class User(AbstractUser):
|
|
# Override inherited fields to remove them
|
|
first_name = None
|
|
last_name = None
|
|
|
|
# Read-only ID
|
|
user_id = models.CharField(
|
|
max_length=10,
|
|
unique=True,
|
|
editable=False,
|
|
help_text=(
|
|
"Unique identifier for this user that remains constant even if the "
|
|
"username changes"
|
|
),
|
|
)
|
|
|
|
role = RichChoiceField(
|
|
choice_group="user_roles",
|
|
domain="accounts",
|
|
max_length=10,
|
|
default="USER",
|
|
db_index=True,
|
|
help_text="User role (user, moderator, admin)",
|
|
)
|
|
is_banned = models.BooleanField(
|
|
default=False, db_index=True, help_text="Whether this user is banned"
|
|
)
|
|
ban_reason = models.TextField(blank=True, help_text="Reason for ban")
|
|
ban_date = models.DateTimeField(
|
|
null=True, blank=True, help_text="Date the user was banned"
|
|
)
|
|
pending_email = models.EmailField(blank=True, null=True)
|
|
theme_preference = RichChoiceField(
|
|
choice_group="theme_preferences",
|
|
domain="accounts",
|
|
max_length=5,
|
|
default="light",
|
|
help_text="User's theme preference (light/dark)",
|
|
)
|
|
|
|
# Notification preferences
|
|
email_notifications = models.BooleanField(
|
|
default=True, help_text="Whether to send email notifications"
|
|
)
|
|
push_notifications = models.BooleanField(
|
|
default=False, help_text="Whether to send push notifications"
|
|
)
|
|
|
|
# Privacy settings
|
|
privacy_level = RichChoiceField(
|
|
choice_group="privacy_levels",
|
|
domain="accounts",
|
|
max_length=10,
|
|
default="public",
|
|
help_text="Overall privacy level",
|
|
)
|
|
show_email = models.BooleanField(
|
|
default=False, help_text="Whether to show email on profile"
|
|
)
|
|
show_real_name = models.BooleanField(
|
|
default=True, help_text="Whether to show real name on profile"
|
|
)
|
|
show_join_date = models.BooleanField(
|
|
default=True, help_text="Whether to show join date on profile"
|
|
)
|
|
show_statistics = models.BooleanField(
|
|
default=True, help_text="Whether to show statistics on profile"
|
|
)
|
|
show_reviews = models.BooleanField(
|
|
default=True, help_text="Whether to show reviews on profile"
|
|
)
|
|
show_photos = models.BooleanField(
|
|
default=True, help_text="Whether to show photos on profile"
|
|
)
|
|
show_top_lists = models.BooleanField(
|
|
default=True, help_text="Whether to show top lists on profile"
|
|
)
|
|
allow_friend_requests = models.BooleanField(
|
|
default=True, help_text="Whether to allow friend requests"
|
|
)
|
|
allow_messages = models.BooleanField(
|
|
default=True, help_text="Whether to allow direct messages"
|
|
)
|
|
allow_profile_comments = models.BooleanField(
|
|
default=False, help_text="Whether to allow profile comments"
|
|
)
|
|
search_visibility = models.BooleanField(
|
|
default=True, help_text="Whether profile appears in search results"
|
|
)
|
|
activity_visibility = RichChoiceField(
|
|
choice_group="privacy_levels",
|
|
domain="accounts",
|
|
max_length=10,
|
|
default="friends",
|
|
help_text="Who can see user activity",
|
|
)
|
|
|
|
# Security settings
|
|
two_factor_enabled = models.BooleanField(
|
|
default=False, help_text="Whether two-factor authentication is enabled"
|
|
)
|
|
login_notifications = models.BooleanField(
|
|
default=True, help_text="Whether to send login notifications"
|
|
)
|
|
session_timeout = models.IntegerField(
|
|
default=30, help_text="Session timeout in days"
|
|
)
|
|
login_history_retention = models.IntegerField(
|
|
default=90, help_text="How long to retain login history (days)"
|
|
)
|
|
last_password_change = models.DateTimeField(
|
|
auto_now_add=True, help_text="When the password was last changed"
|
|
)
|
|
|
|
# Display name - core user data for better performance
|
|
display_name = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text="Display name shown throughout the site. Falls back to username if not set.",
|
|
)
|
|
|
|
# Detailed notification preferences (JSON field for flexibility)
|
|
notification_preferences = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="Detailed notification preferences stored as JSON",
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.get_display_name()
|
|
|
|
def get_absolute_url(self):
|
|
return reverse("profile", kwargs={"username": self.username})
|
|
|
|
def get_display_name(self):
|
|
"""Get the user's display name, falling back to username if not set"""
|
|
if self.display_name:
|
|
return self.display_name
|
|
# Fallback to profile display_name for backward compatibility
|
|
profile = getattr(self, "profile", None)
|
|
if profile and profile.display_name:
|
|
return profile.display_name
|
|
return self.username
|
|
|
|
class Meta:
|
|
verbose_name = "User"
|
|
verbose_name_plural = "Users"
|
|
indexes = [
|
|
models.Index(fields=['is_banned', 'role'], name='accounts_user_banned_role_idx'),
|
|
]
|
|
constraints = [
|
|
models.CheckConstraint(
|
|
name='user_ban_consistency',
|
|
check=models.Q(is_banned=False) | models.Q(ban_date__isnull=False),
|
|
violation_error_message='Banned users must have a ban_date set'
|
|
),
|
|
]
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.user_id:
|
|
self.user_id = generate_random_id(User, "user_id")
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
@pghistory.track()
|
|
class UserProfile(models.Model):
|
|
# Read-only ID
|
|
profile_id = models.CharField(
|
|
max_length=10,
|
|
unique=True,
|
|
editable=False,
|
|
help_text="Unique identifier for this profile that remains constant",
|
|
)
|
|
|
|
user = models.OneToOneField(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name="profile",
|
|
help_text="User this profile belongs to",
|
|
)
|
|
display_name = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text="Legacy display name field - use User.display_name instead",
|
|
)
|
|
avatar = models.ForeignKey(
|
|
'django_cloudflareimages_toolkit.CloudflareImage',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
help_text="User's avatar image",
|
|
)
|
|
pronouns = models.CharField(
|
|
max_length=50, blank=True, help_text="User's preferred pronouns"
|
|
)
|
|
|
|
bio = models.TextField(max_length=500, blank=True, help_text="User biography")
|
|
|
|
# Social media links
|
|
twitter = models.URLField(blank=True, help_text="Twitter profile URL")
|
|
instagram = models.URLField(blank=True, help_text="Instagram profile URL")
|
|
youtube = models.URLField(blank=True, help_text="YouTube channel URL")
|
|
discord = models.CharField(max_length=100, blank=True, help_text="Discord username")
|
|
|
|
# Ride statistics
|
|
coaster_credits = models.IntegerField(
|
|
default=0, help_text="Number of roller coasters ridden"
|
|
)
|
|
dark_ride_credits = models.IntegerField(
|
|
default=0, help_text="Number of dark rides ridden"
|
|
)
|
|
flat_ride_credits = models.IntegerField(
|
|
default=0, help_text="Number of flat rides ridden"
|
|
)
|
|
water_ride_credits = models.IntegerField(
|
|
default=0, help_text="Number of water rides ridden"
|
|
)
|
|
|
|
def get_avatar_url(self):
|
|
"""
|
|
Return the avatar URL or generate a default letter-based avatar URL
|
|
"""
|
|
if self.avatar and self.avatar.is_uploaded:
|
|
# Try to get avatar variant first, fallback to public
|
|
avatar_url = self.avatar.get_url('avatar')
|
|
if avatar_url:
|
|
return avatar_url
|
|
|
|
# Fallback to public variant
|
|
public_url = self.avatar.get_url('public')
|
|
if public_url:
|
|
return public_url
|
|
|
|
# Last fallback - try any available variant
|
|
if self.avatar.variants:
|
|
if isinstance(self.avatar.variants, list) and self.avatar.variants:
|
|
return self.avatar.variants[0]
|
|
elif isinstance(self.avatar.variants, dict):
|
|
# Return first available variant
|
|
for variant_url in self.avatar.variants.values():
|
|
if variant_url:
|
|
return variant_url
|
|
|
|
# Generate default letter-based avatar using first letter of username
|
|
first_letter = self.user.username[0].upper() if self.user.username else "U"
|
|
# Use a service like UI Avatars or generate a simple colored avatar
|
|
return f"https://ui-avatars.com/api/?name={first_letter}&size=200&background=random&color=fff&bold=true"
|
|
|
|
def get_avatar_variants(self):
|
|
"""
|
|
Return avatar variants for different use cases
|
|
"""
|
|
if self.avatar and self.avatar.is_uploaded:
|
|
variants = {}
|
|
|
|
# Try to get specific variants
|
|
thumbnail_url = self.avatar.get_url('thumbnail')
|
|
avatar_url = self.avatar.get_url('avatar')
|
|
large_url = self.avatar.get_url('large')
|
|
public_url = self.avatar.get_url('public')
|
|
|
|
# Use specific variants if available, otherwise fallback to public or first available
|
|
fallback_url = public_url
|
|
if not fallback_url and self.avatar.variants:
|
|
if isinstance(self.avatar.variants, list) and self.avatar.variants:
|
|
fallback_url = self.avatar.variants[0]
|
|
elif isinstance(self.avatar.variants, dict):
|
|
fallback_url = next(iter(self.avatar.variants.values()), None)
|
|
|
|
variants = {
|
|
"thumbnail": thumbnail_url or fallback_url,
|
|
"avatar": avatar_url or fallback_url,
|
|
"large": large_url or fallback_url,
|
|
}
|
|
|
|
# Only return variants if we have at least one valid URL
|
|
if any(variants.values()):
|
|
return variants
|
|
|
|
# For default avatars, return the same URL for all variants
|
|
default_url = self.get_avatar_url()
|
|
return {
|
|
"thumbnail": default_url,
|
|
"avatar": default_url,
|
|
"large": default_url,
|
|
}
|
|
|
|
def save(self, *args, **kwargs):
|
|
# If no display name is set, use the username
|
|
if not self.display_name:
|
|
self.display_name = self.user.username
|
|
|
|
if not self.profile_id:
|
|
self.profile_id = generate_random_id(UserProfile, "profile_id")
|
|
super().save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return self.display_name
|
|
|
|
class Meta:
|
|
verbose_name = "User Profile"
|
|
verbose_name_plural = "User Profiles"
|
|
ordering = ["user"]
|
|
|
|
|
|
@pghistory.track()
|
|
class EmailVerification(models.Model):
|
|
user = models.OneToOneField(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
help_text="User this verification belongs to",
|
|
)
|
|
token = models.CharField(
|
|
max_length=64, unique=True, help_text="Verification token"
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True, help_text="When this verification was created"
|
|
)
|
|
last_sent = models.DateTimeField(
|
|
auto_now_add=True, help_text="When the verification email was last sent"
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"Email verification for {self.user.username}"
|
|
|
|
class Meta:
|
|
verbose_name = "Email Verification"
|
|
verbose_name_plural = "Email Verifications"
|
|
|
|
|
|
@pghistory.track()
|
|
class PasswordReset(models.Model):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
help_text="User requesting password reset",
|
|
)
|
|
token = models.CharField(max_length=64, help_text="Reset token")
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True, help_text="When this reset was requested"
|
|
)
|
|
expires_at = models.DateTimeField(help_text="When this reset token expires")
|
|
used = models.BooleanField(default=False, help_text="Whether this token has been used")
|
|
|
|
def __str__(self):
|
|
return f"Password reset for {self.user.username}"
|
|
|
|
class Meta:
|
|
verbose_name = "Password Reset"
|
|
verbose_name_plural = "Password Resets"
|
|
|
|
|
|
# @pghistory.track()
|
|
|
|
|
|
class TopList(TrackedModel):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name="top_lists",
|
|
help_text="User who created this list",
|
|
)
|
|
title = models.CharField(max_length=100, help_text="Title of the top list")
|
|
category = RichChoiceField(
|
|
choice_group="top_list_categories",
|
|
domain="accounts",
|
|
max_length=2,
|
|
help_text="Category of items in this list",
|
|
)
|
|
description = models.TextField(blank=True, help_text="Description of the list")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta(TrackedModel.Meta):
|
|
verbose_name = "Top List"
|
|
verbose_name_plural = "Top Lists"
|
|
ordering = ["-updated_at"]
|
|
|
|
def __str__(self):
|
|
return (
|
|
f"{self.user.get_display_name()}'s {self.category} Top List: {self.title}"
|
|
)
|
|
|
|
|
|
# @pghistory.track()
|
|
|
|
|
|
class TopListItem(TrackedModel):
|
|
top_list = models.ForeignKey(
|
|
TopList,
|
|
on_delete=models.CASCADE,
|
|
related_name="items",
|
|
help_text="Top list this item belongs to",
|
|
)
|
|
content_type = models.ForeignKey(
|
|
"contenttypes.ContentType",
|
|
on_delete=models.CASCADE,
|
|
help_text="Type of item (park, ride, etc.)",
|
|
)
|
|
object_id = models.PositiveIntegerField(help_text="ID of the item")
|
|
rank = models.PositiveIntegerField(help_text="Position in the list")
|
|
notes = models.TextField(blank=True, help_text="User's notes about this item")
|
|
|
|
class Meta(TrackedModel.Meta):
|
|
verbose_name = "Top List Item"
|
|
verbose_name_plural = "Top List Items"
|
|
ordering = ["rank"]
|
|
unique_together = [["top_list", "rank"]]
|
|
|
|
def __str__(self):
|
|
return f"#{self.rank} in {self.top_list.title}"
|
|
|
|
|
|
@pghistory.track()
|
|
class UserDeletionRequest(models.Model):
|
|
"""
|
|
Model to track user deletion requests with email verification.
|
|
|
|
When a user requests to delete their account, a verification code
|
|
is sent to their email. The deletion is only processed when they
|
|
provide the correct code.
|
|
"""
|
|
|
|
user = models.OneToOneField(
|
|
User, on_delete=models.CASCADE, related_name="deletion_request"
|
|
)
|
|
|
|
verification_code = models.CharField(
|
|
max_length=32,
|
|
unique=True,
|
|
help_text="Unique verification code sent to user's email",
|
|
)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
expires_at = models.DateTimeField(help_text="When this deletion request expires")
|
|
|
|
email_sent_at = models.DateTimeField(
|
|
null=True, blank=True, help_text="When the verification email was sent"
|
|
)
|
|
|
|
attempts = models.PositiveIntegerField(
|
|
default=0, help_text="Number of verification attempts made"
|
|
)
|
|
|
|
max_attempts = models.PositiveIntegerField(
|
|
default=5, help_text="Maximum number of verification attempts allowed"
|
|
)
|
|
|
|
is_used = models.BooleanField(
|
|
default=False, help_text="Whether this deletion request has been used"
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "User Deletion Request"
|
|
verbose_name_plural = "User Deletion Requests"
|
|
ordering = ["-created_at"]
|
|
indexes = [
|
|
models.Index(fields=["verification_code"]),
|
|
models.Index(fields=["expires_at"]),
|
|
models.Index(fields=["user", "is_used"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Deletion request for {self.user.username} - {self.verification_code}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.verification_code:
|
|
self.verification_code = self.generate_verification_code()
|
|
|
|
if not self.expires_at:
|
|
# Deletion requests expire after 24 hours
|
|
self.expires_at = timezone.now() + timedelta(hours=24)
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@staticmethod
|
|
def generate_verification_code():
|
|
"""Generate a unique 8-character verification code."""
|
|
while True:
|
|
# Generate a random 8-character alphanumeric code
|
|
code = "".join(
|
|
secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(8)
|
|
)
|
|
|
|
# Ensure it's unique
|
|
if not UserDeletionRequest.objects.filter(verification_code=code).exists():
|
|
return code
|
|
|
|
def is_expired(self):
|
|
"""Check if this deletion request has expired."""
|
|
return timezone.now() > self.expires_at
|
|
|
|
def is_valid(self):
|
|
"""Check if this deletion request is still valid."""
|
|
return (
|
|
not self.is_used
|
|
and not self.is_expired()
|
|
and self.attempts < self.max_attempts
|
|
)
|
|
|
|
def increment_attempts(self):
|
|
"""Increment the number of verification attempts."""
|
|
self.attempts += 1
|
|
self.save(update_fields=["attempts"])
|
|
|
|
def mark_as_used(self):
|
|
"""Mark this deletion request as used."""
|
|
self.is_used = True
|
|
self.save(update_fields=["is_used"])
|
|
|
|
@classmethod
|
|
def cleanup_expired(cls):
|
|
"""Remove expired deletion requests."""
|
|
expired_requests = cls.objects.filter(
|
|
expires_at__lt=timezone.now(), is_used=False
|
|
)
|
|
count = expired_requests.count()
|
|
expired_requests.delete()
|
|
return count
|
|
|
|
|
|
@pghistory.track()
|
|
class UserNotification(TrackedModel):
|
|
"""
|
|
Model to store user notifications for various events.
|
|
|
|
This includes submission approvals, rejections, system announcements,
|
|
and other user-relevant notifications.
|
|
"""
|
|
|
|
# Core fields
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name="notifications",
|
|
help_text="User this notification is for",
|
|
)
|
|
|
|
notification_type = RichChoiceField(
|
|
choice_group="notification_types",
|
|
domain="accounts",
|
|
max_length=30,
|
|
)
|
|
|
|
title = models.CharField(max_length=200, help_text="Notification title")
|
|
message = models.TextField(help_text="Notification message")
|
|
|
|
# Optional related object (submission, review, etc.)
|
|
content_type = models.ForeignKey(
|
|
"contenttypes.ContentType",
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Type of related object",
|
|
)
|
|
object_id = models.PositiveIntegerField(
|
|
null=True, blank=True, help_text="ID of related object"
|
|
)
|
|
related_object = GenericForeignKey("content_type", "object_id")
|
|
|
|
# Metadata
|
|
priority = RichChoiceField(
|
|
choice_group="notification_priorities",
|
|
domain="accounts",
|
|
max_length=10,
|
|
default="normal",
|
|
)
|
|
|
|
# Status tracking
|
|
is_read = models.BooleanField(
|
|
default=False, help_text="Whether this notification has been read"
|
|
)
|
|
read_at = models.DateTimeField(
|
|
null=True, blank=True, help_text="When this notification was read"
|
|
)
|
|
|
|
# Delivery tracking
|
|
email_sent = models.BooleanField(default=False, help_text="Whether email was sent")
|
|
email_sent_at = models.DateTimeField(
|
|
null=True, blank=True, help_text="When email was sent"
|
|
)
|
|
push_sent = models.BooleanField(
|
|
default=False, help_text="Whether push notification was sent"
|
|
)
|
|
push_sent_at = models.DateTimeField(
|
|
null=True, blank=True, help_text="When push notification was sent"
|
|
)
|
|
|
|
# Additional data (JSON field for flexibility)
|
|
extra_data = models.JSONField(default=dict, blank=True)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
expires_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta(TrackedModel.Meta):
|
|
verbose_name = "User Notification"
|
|
verbose_name_plural = "User Notifications"
|
|
ordering = ["-created_at"]
|
|
indexes = [
|
|
models.Index(fields=["user", "is_read"]),
|
|
models.Index(fields=["user", "notification_type"]),
|
|
models.Index(fields=["created_at"]),
|
|
models.Index(fields=["expires_at"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username}: {self.title}"
|
|
|
|
def mark_as_read(self):
|
|
"""Mark notification as read."""
|
|
if not self.is_read:
|
|
self.is_read = True
|
|
self.read_at = timezone.now()
|
|
self.save(update_fields=["is_read", "read_at"])
|
|
|
|
def is_expired(self):
|
|
"""Check if notification has expired."""
|
|
if not self.expires_at:
|
|
return False
|
|
return timezone.now() > self.expires_at
|
|
|
|
@classmethod
|
|
def cleanup_expired(cls):
|
|
"""Remove expired notifications."""
|
|
expired_notifications = cls.objects.filter(expires_at__lt=timezone.now())
|
|
count = expired_notifications.count()
|
|
expired_notifications.delete()
|
|
return count
|
|
|
|
@classmethod
|
|
def mark_all_read_for_user(cls, user):
|
|
"""Mark all notifications as read for a specific user."""
|
|
return cls.objects.filter(user=user, is_read=False).update(
|
|
is_read=True, read_at=timezone.now()
|
|
)
|
|
|
|
|
|
@pghistory.track()
|
|
class NotificationPreference(TrackedModel):
|
|
"""
|
|
User preferences for different types of notifications.
|
|
|
|
This allows users to control which notifications they receive
|
|
and through which channels (email, push, in-app).
|
|
"""
|
|
|
|
user = models.OneToOneField(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name="notification_preference",
|
|
help_text="User these preferences belong to",
|
|
)
|
|
|
|
# Submission notifications
|
|
submission_approved_email = models.BooleanField(default=True)
|
|
submission_approved_push = models.BooleanField(default=True)
|
|
submission_approved_inapp = models.BooleanField(default=True)
|
|
|
|
submission_rejected_email = models.BooleanField(default=True)
|
|
submission_rejected_push = models.BooleanField(default=True)
|
|
submission_rejected_inapp = models.BooleanField(default=True)
|
|
|
|
submission_pending_email = models.BooleanField(default=False)
|
|
submission_pending_push = models.BooleanField(default=False)
|
|
submission_pending_inapp = models.BooleanField(default=True)
|
|
|
|
# Review notifications
|
|
review_reply_email = models.BooleanField(default=True)
|
|
review_reply_push = models.BooleanField(default=True)
|
|
review_reply_inapp = models.BooleanField(default=True)
|
|
|
|
review_helpful_email = models.BooleanField(default=False)
|
|
review_helpful_push = models.BooleanField(default=True)
|
|
review_helpful_inapp = models.BooleanField(default=True)
|
|
|
|
# Social notifications
|
|
friend_request_email = models.BooleanField(default=True)
|
|
friend_request_push = models.BooleanField(default=True)
|
|
friend_request_inapp = models.BooleanField(default=True)
|
|
|
|
friend_accepted_email = models.BooleanField(default=False)
|
|
friend_accepted_push = models.BooleanField(default=True)
|
|
friend_accepted_inapp = models.BooleanField(default=True)
|
|
|
|
message_received_email = models.BooleanField(default=True)
|
|
message_received_push = models.BooleanField(default=True)
|
|
message_received_inapp = models.BooleanField(default=True)
|
|
|
|
# System notifications
|
|
system_announcement_email = models.BooleanField(default=True)
|
|
system_announcement_push = models.BooleanField(default=False)
|
|
system_announcement_inapp = models.BooleanField(default=True)
|
|
|
|
account_security_email = models.BooleanField(default=True)
|
|
account_security_push = models.BooleanField(default=True)
|
|
account_security_inapp = models.BooleanField(default=True)
|
|
|
|
feature_update_email = models.BooleanField(default=True)
|
|
feature_update_push = models.BooleanField(default=False)
|
|
feature_update_inapp = models.BooleanField(default=True)
|
|
|
|
# Achievement notifications
|
|
achievement_unlocked_email = models.BooleanField(default=False)
|
|
achievement_unlocked_push = models.BooleanField(default=True)
|
|
achievement_unlocked_inapp = models.BooleanField(default=True)
|
|
|
|
milestone_reached_email = models.BooleanField(default=False)
|
|
milestone_reached_push = models.BooleanField(default=True)
|
|
milestone_reached_inapp = models.BooleanField(default=True)
|
|
|
|
class Meta(TrackedModel.Meta):
|
|
verbose_name = "Notification Preference"
|
|
verbose_name_plural = "Notification Preferences"
|
|
|
|
def __str__(self):
|
|
return f"Notification preferences for {self.user.username}"
|
|
|
|
def should_send_notification(self, notification_type, channel):
|
|
"""
|
|
Check if a notification should be sent for a specific type and channel.
|
|
|
|
Args:
|
|
notification_type: The type of notification (from UserNotification.NotificationType)
|
|
channel: The delivery channel ('email', 'push', 'inapp')
|
|
|
|
Returns:
|
|
bool: True if notification should be sent, False otherwise
|
|
"""
|
|
field_name = f"{notification_type}_{channel}"
|
|
return getattr(self, field_name, False)
|
|
|
|
|
|
# Signal handlers for automatic notification preference creation
|
|
|
|
|
|
@receiver(post_save, sender=User)
|
|
def create_notification_preference(sender, instance, created, **kwargs):
|
|
"""Create notification preferences when a new user is created."""
|
|
if created:
|
|
NotificationPreference.objects.create(user=instance)
|