mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:51:09 -05:00
213 lines
6.9 KiB
Python
213 lines
6.9 KiB
Python
from django.contrib.auth.models import AbstractUser
|
|
from django.db import models
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
from io import BytesIO
|
|
import base64
|
|
import os
|
|
import secrets
|
|
from history_tracking.models import TrackedModel
|
|
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
|
|
|
|
class User(AbstractUser):
|
|
class Roles(models.TextChoices):
|
|
USER = 'USER', _('User')
|
|
MODERATOR = 'MODERATOR', _('Moderator')
|
|
ADMIN = 'ADMIN', _('Admin')
|
|
SUPERUSER = 'SUPERUSER', _('Superuser')
|
|
|
|
class ThemePreference(models.TextChoices):
|
|
LIGHT = 'light', _('Light')
|
|
DARK = 'dark', _('Dark')
|
|
|
|
# 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 = models.CharField(
|
|
max_length=10,
|
|
choices=Roles.choices,
|
|
default=Roles.USER,
|
|
)
|
|
is_banned = models.BooleanField(default=False)
|
|
ban_reason = models.TextField(blank=True)
|
|
ban_date = models.DateTimeField(null=True, blank=True)
|
|
pending_email = models.EmailField(blank=True, null=True)
|
|
theme_preference = models.CharField(
|
|
max_length=5,
|
|
choices=ThemePreference.choices,
|
|
default=ThemePreference.LIGHT,
|
|
)
|
|
|
|
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"""
|
|
profile = getattr(self, 'profile', None)
|
|
if profile and profile.display_name:
|
|
return profile.display_name
|
|
return self.username
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.user_id:
|
|
self.user_id = generate_random_id(User, 'user_id')
|
|
super().save(*args, **kwargs)
|
|
|
|
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'
|
|
)
|
|
display_name = models.CharField(
|
|
max_length=50,
|
|
unique=True,
|
|
help_text="This is the name that will be displayed on the site"
|
|
)
|
|
avatar = models.ImageField(upload_to='avatars/', blank=True)
|
|
pronouns = models.CharField(max_length=50, blank=True)
|
|
|
|
bio = models.TextField(max_length=500, blank=True)
|
|
|
|
# Social media links
|
|
twitter = models.URLField(blank=True)
|
|
instagram = models.URLField(blank=True)
|
|
youtube = models.URLField(blank=True)
|
|
discord = models.CharField(max_length=100, blank=True)
|
|
|
|
# Ride statistics
|
|
coaster_credits = models.IntegerField(default=0)
|
|
dark_ride_credits = models.IntegerField(default=0)
|
|
flat_ride_credits = models.IntegerField(default=0)
|
|
water_ride_credits = models.IntegerField(default=0)
|
|
|
|
def get_avatar(self):
|
|
"""Return the avatar URL or serve a pre-generated avatar based on the first letter of the username"""
|
|
if self.avatar:
|
|
return self.avatar.url
|
|
first_letter = self.user.username[0].upper()
|
|
avatar_path = f"avatars/letters/{first_letter}_avatar.png"
|
|
if os.path.exists(avatar_path):
|
|
return f"/{avatar_path}"
|
|
return "/static/images/default-avatar.png"
|
|
|
|
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 EmailVerification(models.Model):
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
|
token = models.CharField(max_length=64, unique=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
last_sent = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f"Email verification for {self.user.username}"
|
|
|
|
class Meta:
|
|
verbose_name = "Email Verification"
|
|
verbose_name_plural = "Email Verifications"
|
|
|
|
class PasswordReset(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
token = models.CharField(max_length=64)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
expires_at = models.DateTimeField()
|
|
used = models.BooleanField(default=False)
|
|
|
|
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):
|
|
class Categories(models.TextChoices):
|
|
ROLLER_COASTER = 'RC', _('Roller Coaster')
|
|
DARK_RIDE = 'DR', _('Dark Ride')
|
|
FLAT_RIDE = 'FR', _('Flat Ride')
|
|
WATER_RIDE = 'WR', _('Water Ride')
|
|
PARK = 'PK', _('Park')
|
|
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='top_lists' # Added related_name for User model access
|
|
)
|
|
title = models.CharField(max_length=100)
|
|
category = models.CharField(
|
|
max_length=2,
|
|
choices=Categories.choices
|
|
)
|
|
description = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
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'
|
|
)
|
|
content_type = models.ForeignKey(
|
|
'contenttypes.ContentType',
|
|
on_delete=models.CASCADE
|
|
)
|
|
object_id = models.PositiveIntegerField()
|
|
rank = models.PositiveIntegerField()
|
|
notes = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['rank']
|
|
unique_together = [['top_list', 'rank']]
|
|
|
|
def __str__(self):
|
|
return f"#{self.rank} in {self.top_list.title}"
|