""" User models for ThrillWiki. Custom user model with OAuth and MFA support. """ import uuid from django.contrib.auth.models import AbstractUser from django.db import models from apps.core.models import BaseModel class User(AbstractUser): """ Custom user model with UUID primary key and additional fields. Supports: - Email-based authentication - OAuth (Google, Discord) - Two-factor authentication (TOTP) - User reputation and moderation """ # Override id to use UUID id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) # Email as primary identifier email = models.EmailField( unique=True, help_text="Email address for authentication" ) # OAuth fields oauth_provider = models.CharField( max_length=50, blank=True, choices=[ ('', 'None'), ('google', 'Google'), ('discord', 'Discord'), ], help_text="OAuth provider used for authentication" ) oauth_sub = models.CharField( max_length=255, blank=True, help_text="OAuth subject identifier from provider" ) # MFA fields mfa_enabled = models.BooleanField( default=False, help_text="Whether two-factor authentication is enabled" ) # Profile fields avatar_url = models.URLField( blank=True, help_text="URL to user's avatar image" ) bio = models.TextField( blank=True, max_length=500, help_text="User biography" ) # Moderation fields 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" ) banned_at = models.DateTimeField( null=True, blank=True, help_text="When the user was banned" ) banned_by = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='users_banned', help_text="Moderator who banned this user" ) # Reputation system reputation_score = models.IntegerField( default=0, help_text="User reputation score based on contributions" ) # Timestamps (inherited from AbstractUser) # date_joined, last_login # Use email for authentication USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] class Meta: db_table = 'users' ordering = ['-date_joined'] indexes = [ models.Index(fields=['email']), models.Index(fields=['banned']), ] def __str__(self): return self.email def ban(self, reason, banned_by=None): """Ban this user""" from django.utils import timezone self.banned = True self.ban_reason = reason self.banned_at = timezone.now() self.banned_by = banned_by self.save(update_fields=['banned', 'ban_reason', 'banned_at', 'banned_by']) def unban(self): """Unban this user""" self.banned = False self.ban_reason = '' self.banned_at = None self.banned_by = None self.save(update_fields=['banned', 'ban_reason', 'banned_at', 'banned_by']) @property def display_name(self): """Return the user's display name (full name or username)""" if self.first_name or self.last_name: return f"{self.first_name} {self.last_name}".strip() return self.username or self.email.split('@')[0] class UserRole(BaseModel): """ User role assignments for permission management. Roles: - user: Standard user (default) - moderator: Can approve submissions and moderate content - admin: Full access to admin features """ ROLE_CHOICES = [ ('user', 'User'), ('moderator', 'Moderator'), ('admin', 'Admin'), ] user = models.OneToOneField( User, on_delete=models.CASCADE, related_name='role' ) role = models.CharField( max_length=20, choices=ROLE_CHOICES, default='user', db_index=True ) granted_at = models.DateTimeField(auto_now_add=True) granted_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='roles_granted' ) class Meta: db_table = 'user_roles' def __str__(self): return f"{self.user.email} - {self.role}" @property def is_moderator(self): """Check if user is a moderator or admin""" return self.role in ['moderator', 'admin'] @property def is_admin(self): """Check if user is an admin""" return self.role == 'admin' class UserProfile(BaseModel): """ Extended user profile information. Stores additional user preferences and settings. """ user = models.OneToOneField( User, on_delete=models.CASCADE, related_name='profile' ) # Preferences email_notifications = models.BooleanField( default=True, help_text="Receive email notifications" ) email_on_submission_approved = models.BooleanField( default=True, help_text="Email when submissions are approved" ) email_on_submission_rejected = models.BooleanField( default=True, help_text="Email when submissions are rejected" ) # Privacy profile_public = models.BooleanField( default=True, help_text="Make profile publicly visible" ) show_email = models.BooleanField( default=False, help_text="Show email on public profile" ) # Statistics total_submissions = models.IntegerField( default=0, help_text="Total number of submissions made" ) approved_submissions = models.IntegerField( default=0, help_text="Number of approved submissions" ) class Meta: db_table = 'user_profiles' def __str__(self): return f"Profile for {self.user.email}" def update_submission_stats(self): """Update submission statistics""" from apps.moderation.models import ContentSubmission self.total_submissions = ContentSubmission.objects.filter(user=self.user).count() self.approved_submissions = ContentSubmission.objects.filter( user=self.user, status='approved' ).count() self.save(update_fields=['total_submissions', 'approved_submissions']) class UserRideCredit(BaseModel): """ Track which rides users have ridden (ride credits/coaster counting). Users can log which rides they've been on and track their first ride date. """ user = models.ForeignKey( User, on_delete=models.CASCADE, related_name='ride_credits' ) ride = models.ForeignKey( 'entities.Ride', on_delete=models.CASCADE, related_name='user_credits' ) # First ride date first_ride_date = models.DateField( null=True, blank=True, help_text="Date of first ride" ) # Ride count for this specific ride ride_count = models.PositiveIntegerField( default=1, help_text="Number of times user has ridden this ride" ) # Notes about the ride experience notes = models.TextField( blank=True, help_text="User notes about this ride" ) class Meta: db_table = 'user_ride_credits' unique_together = [['user', 'ride']] ordering = ['-first_ride_date', '-created'] indexes = [ models.Index(fields=['user', 'first_ride_date']), models.Index(fields=['ride']), ] def __str__(self): return f"{self.user.username} - {self.ride.name}" @property def park(self): """Get the park this ride is at""" return self.ride.park class UserTopList(BaseModel): """ User-created ranked lists (top parks, top rides, top coasters, etc.). Users can create and share their personal rankings of parks, rides, and other entities. """ LIST_TYPE_CHOICES = [ ('parks', 'Parks'), ('rides', 'Rides'), ('coasters', 'Coasters'), ] user = models.ForeignKey( User, on_delete=models.CASCADE, related_name='top_lists' ) # List metadata list_type = models.CharField( max_length=20, choices=LIST_TYPE_CHOICES, db_index=True, help_text="Type of entities in this list" ) title = models.CharField( max_length=200, help_text="Title of the list" ) description = models.TextField( blank=True, help_text="Description of the list" ) # Privacy is_public = models.BooleanField( default=True, db_index=True, help_text="Whether this list is publicly visible" ) class Meta: db_table = 'user_top_lists' ordering = ['-created'] indexes = [ models.Index(fields=['user', 'list_type']), models.Index(fields=['is_public', 'created']), ] def __str__(self): return f"{self.user.username} - {self.title}" @property def item_count(self): """Get the number of items in this list""" return self.items.count() class UserTopListItem(BaseModel): """ Individual items in a user's top list with position and notes. """ top_list = models.ForeignKey( UserTopList, on_delete=models.CASCADE, related_name='items' ) # Generic relation to park or ride from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, limit_choices_to={'model__in': ('park', 'ride')} ) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') # Position in list (1 = top) position = models.PositiveIntegerField( help_text="Position in the list (1 = top)" ) # Optional notes about this specific item notes = models.TextField( blank=True, help_text="User notes about why this item is ranked here" ) class Meta: db_table = 'user_top_list_items' ordering = ['position'] unique_together = [['top_list', 'position']] indexes = [ models.Index(fields=['top_list', 'position']), models.Index(fields=['content_type', 'object_id']), ] def __str__(self): entity_name = str(self.content_object) if self.content_object else f"ID {self.object_id}" return f"#{self.position}: {entity_name}"