""" 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'])