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 core.history 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.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}"