Files
thrilltrack-explorer/django/apps/users/models.py
pacnpal 00985eac8d Implement reviews and voting system
- Added Review model with fields for user, content type, title, content, rating, visit metadata, helpful votes, moderation status, and timestamps.
- Created ReviewHelpfulVote model to track user votes on reviews.
- Implemented moderation workflow for reviews with approve and reject methods.
- Developed admin interface for managing reviews and helpful votes, including custom display methods and actions for bulk approval/rejection.
- Added migrations for the new models and their relationships.
- Ensured unique constraints and indexes for efficient querying.
2025-11-08 15:50:43 -05:00

420 lines
11 KiB
Python

"""
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}"