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 _ 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(TrackedModel.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(TrackedModel.Meta): ordering = ["rank"] unique_together = [["top_list", "rank"]] def __str__(self): return f"#{self.rank} in {self.top_list.title}"