mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-01 20:27:02 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -41,10 +41,7 @@ class User(AbstractUser):
|
||||
max_length=10,
|
||||
unique=True,
|
||||
editable=False,
|
||||
help_text=(
|
||||
"Unique identifier for this user that remains constant even if the "
|
||||
"username changes"
|
||||
),
|
||||
help_text=("Unique identifier for this user that remains constant even if the " "username changes"),
|
||||
)
|
||||
|
||||
role = RichChoiceField(
|
||||
@@ -55,13 +52,9 @@ class User(AbstractUser):
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
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",
|
||||
@@ -72,12 +65,8 @@ class User(AbstractUser):
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
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(
|
||||
@@ -87,39 +76,17 @@ class User(AbstractUser):
|
||||
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"
|
||||
)
|
||||
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",
|
||||
@@ -129,21 +96,11 @@ class User(AbstractUser):
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
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(
|
||||
@@ -179,13 +136,13 @@ class User(AbstractUser):
|
||||
verbose_name = "User"
|
||||
verbose_name_plural = "Users"
|
||||
indexes = [
|
||||
models.Index(fields=['is_banned', 'role'], name='accounts_user_banned_role_idx'),
|
||||
models.Index(fields=["is_banned", "role"], name="accounts_user_banned_role_idx"),
|
||||
]
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name='user_ban_consistency',
|
||||
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'
|
||||
violation_error_message="Banned users must have a ban_date set",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -224,14 +181,10 @@ class UserProfile(models.Model):
|
||||
related_name="user_profiles",
|
||||
help_text="User's avatar image",
|
||||
)
|
||||
pronouns = models.CharField(
|
||||
max_length=50, blank=True, help_text="User's preferred pronouns"
|
||||
)
|
||||
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")
|
||||
location = models.CharField(
|
||||
max_length=100, blank=True, help_text="User's location (City, Country)"
|
||||
)
|
||||
location = models.CharField(max_length=100, blank=True, help_text="User's location (City, Country)")
|
||||
unit_system = RichChoiceField(
|
||||
choice_group="unit_systems",
|
||||
domain="accounts",
|
||||
@@ -247,18 +200,10 @@ class UserProfile(models.Model):
|
||||
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"
|
||||
)
|
||||
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):
|
||||
"""
|
||||
@@ -266,12 +211,12 @@ class UserProfile(models.Model):
|
||||
"""
|
||||
if self.avatar and self.avatar.is_uploaded:
|
||||
# Try to get avatar variant first, fallback to public
|
||||
avatar_url = self.avatar.get_url('avatar')
|
||||
avatar_url = self.avatar.get_url("avatar")
|
||||
if avatar_url:
|
||||
return avatar_url
|
||||
|
||||
# Fallback to public variant
|
||||
public_url = self.avatar.get_url('public')
|
||||
public_url = self.avatar.get_url("public")
|
||||
if public_url:
|
||||
return public_url
|
||||
|
||||
@@ -298,10 +243,10 @@ class UserProfile(models.Model):
|
||||
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')
|
||||
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
|
||||
@@ -354,18 +299,10 @@ class EmailVerification(models.Model):
|
||||
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"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, help_text="When this verification was last updated"
|
||||
)
|
||||
last_sent = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the verification email was last sent"
|
||||
)
|
||||
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")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="When this verification was last updated")
|
||||
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}"
|
||||
@@ -383,9 +320,7 @@ class PasswordReset(models.Model):
|
||||
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"
|
||||
)
|
||||
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")
|
||||
|
||||
@@ -397,8 +332,6 @@ class PasswordReset(models.Model):
|
||||
verbose_name_plural = "Password Resets"
|
||||
|
||||
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class UserDeletionRequest(models.Model):
|
||||
"""
|
||||
@@ -409,9 +342,7 @@ class UserDeletionRequest(models.Model):
|
||||
provide the correct code.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="deletion_request"
|
||||
)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="deletion_request")
|
||||
|
||||
verification_code = models.CharField(
|
||||
max_length=32,
|
||||
@@ -422,21 +353,13 @@ class UserDeletionRequest(models.Model):
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
is_used = models.BooleanField(default=False, help_text="Whether this deletion request has been used")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User Deletion Request"
|
||||
@@ -466,9 +389,7 @@ class UserDeletionRequest(models.Model):
|
||||
"""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)
|
||||
)
|
||||
code = "".join(secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(8))
|
||||
|
||||
# Ensure it's unique
|
||||
if not UserDeletionRequest.objects.filter(verification_code=code).exists():
|
||||
@@ -480,11 +401,7 @@ class UserDeletionRequest(models.Model):
|
||||
|
||||
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
|
||||
)
|
||||
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."""
|
||||
@@ -499,9 +416,7 @@ class UserDeletionRequest(models.Model):
|
||||
@classmethod
|
||||
def cleanup_expired(cls):
|
||||
"""Remove expired deletion requests."""
|
||||
expired_requests = cls.objects.filter(
|
||||
expires_at__lt=timezone.now(), is_used=False
|
||||
)
|
||||
expired_requests = cls.objects.filter(expires_at__lt=timezone.now(), is_used=False)
|
||||
count = expired_requests.count()
|
||||
expired_requests.delete()
|
||||
return count
|
||||
@@ -541,9 +456,7 @@ class UserNotification(TrackedModel):
|
||||
blank=True,
|
||||
help_text="Type of related object",
|
||||
)
|
||||
object_id = models.PositiveIntegerField(
|
||||
null=True, blank=True, help_text="ID 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
|
||||
@@ -555,24 +468,14 @@ class UserNotification(TrackedModel):
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
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)
|
||||
@@ -619,9 +522,7 @@ class UserNotification(TrackedModel):
|
||||
@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()
|
||||
)
|
||||
return cls.objects.filter(user=user, is_read=False).update(is_read=True, read_at=timezone.now())
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
|
||||
Reference in New Issue
Block a user