mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:11:08 -05:00
Add comprehensive API documentation for ThrillWiki integration and features
- Introduced Next.js integration guide for ThrillWiki API, detailing authentication, core domain APIs, data structures, and implementation patterns. - Documented the migration to Rich Choice Objects, highlighting changes for frontend developers and enhanced metadata availability. - Fixed the missing `get_by_slug` method in the Ride model, ensuring proper functionality of ride detail endpoints. - Created a test script to verify manufacturer syncing with ride models, ensuring data integrity across related models.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Import choices to trigger registration
|
||||
from .choices import *
|
||||
|
||||
563
backend/apps/accounts/choices.py
Normal file
563
backend/apps/accounts/choices.py
Normal file
@@ -0,0 +1,563 @@
|
||||
"""
|
||||
Rich Choice Objects for Accounts Domain
|
||||
|
||||
This module defines all choice objects used in the accounts domain,
|
||||
replacing tuple-based choices with rich, metadata-enhanced choice objects.
|
||||
|
||||
Last updated: 2025-01-15
|
||||
"""
|
||||
|
||||
from apps.core.choices import RichChoice, ChoiceGroup, register_choices
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# USER ROLES
|
||||
# =============================================================================
|
||||
|
||||
user_roles = ChoiceGroup(
|
||||
name="user_roles",
|
||||
choices=[
|
||||
RichChoice(
|
||||
value="USER",
|
||||
label="User",
|
||||
description="Standard user with basic permissions to create content, reviews, and lists",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "user",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"permissions": ["create_content", "create_reviews", "create_lists"],
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="MODERATOR",
|
||||
label="Moderator",
|
||||
description="Trusted user with permissions to moderate content and assist other users",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "shield-check",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"permissions": ["moderate_content", "review_submissions", "manage_reports"],
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="ADMIN",
|
||||
label="Admin",
|
||||
description="Administrator with elevated permissions to manage users and site configuration",
|
||||
metadata={
|
||||
"color": "purple",
|
||||
"icon": "cog",
|
||||
"css_class": "text-purple-600 bg-purple-50",
|
||||
"permissions": ["manage_users", "site_configuration", "advanced_moderation"],
|
||||
"sort_order": 3,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="SUPERUSER",
|
||||
label="Superuser",
|
||||
description="Full system administrator with unrestricted access to all features",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "key",
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"permissions": ["full_access", "system_administration", "database_access"],
|
||||
"sort_order": 4,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# THEME PREFERENCES
|
||||
# =============================================================================
|
||||
|
||||
theme_preferences = ChoiceGroup(
|
||||
name="theme_preferences",
|
||||
choices=[
|
||||
RichChoice(
|
||||
value="light",
|
||||
label="Light",
|
||||
description="Light theme with bright backgrounds and dark text for daytime use",
|
||||
metadata={
|
||||
"color": "yellow",
|
||||
"icon": "sun",
|
||||
"css_class": "text-yellow-600 bg-yellow-50",
|
||||
"preview_colors": {
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937",
|
||||
"accent": "#3b82f6"
|
||||
},
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="dark",
|
||||
label="Dark",
|
||||
description="Dark theme with dark backgrounds and light text for nighttime use",
|
||||
metadata={
|
||||
"color": "gray",
|
||||
"icon": "moon",
|
||||
"css_class": "text-gray-600 bg-gray-50",
|
||||
"preview_colors": {
|
||||
"background": "#1f2937",
|
||||
"text": "#f9fafb",
|
||||
"accent": "#60a5fa"
|
||||
},
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRIVACY LEVELS
|
||||
# =============================================================================
|
||||
|
||||
privacy_levels = ChoiceGroup(
|
||||
name="privacy_levels",
|
||||
choices=[
|
||||
RichChoice(
|
||||
value="public",
|
||||
label="Public",
|
||||
description="Profile and activity visible to all users and search engines",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "globe",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"visibility_scope": "everyone",
|
||||
"search_indexable": True,
|
||||
"implications": [
|
||||
"Profile visible to all users",
|
||||
"Activity appears in public feeds",
|
||||
"Searchable by search engines",
|
||||
"Can be found by username search"
|
||||
],
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="friends",
|
||||
label="Friends Only",
|
||||
description="Profile and activity visible only to accepted friends",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "users",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"visibility_scope": "friends",
|
||||
"search_indexable": False,
|
||||
"implications": [
|
||||
"Profile visible only to friends",
|
||||
"Activity hidden from public feeds",
|
||||
"Not searchable by search engines",
|
||||
"Requires friend request approval"
|
||||
],
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="private",
|
||||
label="Private",
|
||||
description="Profile and activity completely private, visible only to you",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "lock",
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"visibility_scope": "self",
|
||||
"search_indexable": False,
|
||||
"implications": [
|
||||
"Profile completely hidden",
|
||||
"No activity in any feeds",
|
||||
"Not discoverable by other users",
|
||||
"Maximum privacy protection"
|
||||
],
|
||||
"sort_order": 3,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TOP LIST CATEGORIES
|
||||
# =============================================================================
|
||||
|
||||
top_list_categories = ChoiceGroup(
|
||||
name="top_list_categories",
|
||||
choices=[
|
||||
RichChoice(
|
||||
value="RC",
|
||||
label="Roller Coaster",
|
||||
description="Top lists for roller coasters and thrill rides",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "roller-coaster",
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"ride_category": "roller_coaster",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="DR",
|
||||
label="Dark Ride",
|
||||
description="Top lists for dark rides and indoor attractions",
|
||||
metadata={
|
||||
"color": "purple",
|
||||
"icon": "moon",
|
||||
"css_class": "text-purple-600 bg-purple-50",
|
||||
"ride_category": "dark_ride",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="FR",
|
||||
label="Flat Ride",
|
||||
description="Top lists for flat rides and spinning attractions",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "refresh",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"ride_category": "flat_ride",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 3,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="WR",
|
||||
label="Water Ride",
|
||||
description="Top lists for water rides and splash attractions",
|
||||
metadata={
|
||||
"color": "cyan",
|
||||
"icon": "droplet",
|
||||
"css_class": "text-cyan-600 bg-cyan-50",
|
||||
"ride_category": "water_ride",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 4,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="PK",
|
||||
label="Park",
|
||||
description="Top lists for theme parks and amusement parks",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "map",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"entity_type": "park",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 5,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# NOTIFICATION TYPES
|
||||
# =============================================================================
|
||||
|
||||
notification_types = ChoiceGroup(
|
||||
name="notification_types",
|
||||
choices=[
|
||||
# Submission related
|
||||
RichChoice(
|
||||
value="submission_approved",
|
||||
label="Submission Approved",
|
||||
description="Notification when user's submission is approved by moderators",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "check-circle",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"category": "submission",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="submission_rejected",
|
||||
label="Submission Rejected",
|
||||
description="Notification when user's submission is rejected by moderators",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "x-circle",
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"category": "submission",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="submission_pending",
|
||||
label="Submission Pending Review",
|
||||
description="Notification when user's submission is pending moderator review",
|
||||
metadata={
|
||||
"color": "yellow",
|
||||
"icon": "clock",
|
||||
"css_class": "text-yellow-600 bg-yellow-50",
|
||||
"category": "submission",
|
||||
"default_channels": ["inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 3,
|
||||
}
|
||||
),
|
||||
# Review related
|
||||
RichChoice(
|
||||
value="review_reply",
|
||||
label="Review Reply",
|
||||
description="Notification when someone replies to user's review",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "chat-bubble",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"category": "review",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 4,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="review_helpful",
|
||||
label="Review Marked Helpful",
|
||||
description="Notification when user's review is marked as helpful",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "thumbs-up",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"category": "review",
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 5,
|
||||
}
|
||||
),
|
||||
# Social related
|
||||
RichChoice(
|
||||
value="friend_request",
|
||||
label="Friend Request",
|
||||
description="Notification when user receives a friend request",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "user-plus",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"category": "social",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 6,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="friend_accepted",
|
||||
label="Friend Request Accepted",
|
||||
description="Notification when user's friend request is accepted",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "user-check",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"category": "social",
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 7,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="message_received",
|
||||
label="Message Received",
|
||||
description="Notification when user receives a private message",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "mail",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"category": "social",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 8,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="profile_comment",
|
||||
label="Profile Comment",
|
||||
description="Notification when someone comments on user's profile",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "chat",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"category": "social",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 9,
|
||||
}
|
||||
),
|
||||
# System related
|
||||
RichChoice(
|
||||
value="system_announcement",
|
||||
label="System Announcement",
|
||||
description="Important announcements from the ThrillWiki team",
|
||||
metadata={
|
||||
"color": "purple",
|
||||
"icon": "megaphone",
|
||||
"css_class": "text-purple-600 bg-purple-50",
|
||||
"category": "system",
|
||||
"default_channels": ["email", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 10,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="account_security",
|
||||
label="Account Security",
|
||||
description="Security-related notifications for user's account",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "shield-exclamation",
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"category": "system",
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "high",
|
||||
"sort_order": 11,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="feature_update",
|
||||
label="Feature Update",
|
||||
description="Notifications about new features and improvements",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "sparkles",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"category": "system",
|
||||
"default_channels": ["email", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 12,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="maintenance",
|
||||
label="Maintenance Notice",
|
||||
description="Scheduled maintenance and downtime notifications",
|
||||
metadata={
|
||||
"color": "yellow",
|
||||
"icon": "wrench",
|
||||
"css_class": "text-yellow-600 bg-yellow-50",
|
||||
"category": "system",
|
||||
"default_channels": ["email", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 13,
|
||||
}
|
||||
),
|
||||
# Achievement related
|
||||
RichChoice(
|
||||
value="achievement_unlocked",
|
||||
label="Achievement Unlocked",
|
||||
description="Notification when user unlocks a new achievement",
|
||||
metadata={
|
||||
"color": "gold",
|
||||
"icon": "trophy",
|
||||
"css_class": "text-yellow-600 bg-yellow-50",
|
||||
"category": "achievement",
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 14,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="milestone_reached",
|
||||
label="Milestone Reached",
|
||||
description="Notification when user reaches a significant milestone",
|
||||
metadata={
|
||||
"color": "purple",
|
||||
"icon": "flag",
|
||||
"css_class": "text-purple-600 bg-purple-50",
|
||||
"category": "achievement",
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 15,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# NOTIFICATION PRIORITIES
|
||||
# =============================================================================
|
||||
|
||||
notification_priorities = ChoiceGroup(
|
||||
name="notification_priorities",
|
||||
choices=[
|
||||
RichChoice(
|
||||
value="low",
|
||||
label="Low",
|
||||
description="Low priority notifications that can be delayed or batched",
|
||||
metadata={
|
||||
"color": "gray",
|
||||
"icon": "arrow-down",
|
||||
"css_class": "text-gray-600 bg-gray-50",
|
||||
"urgency_level": 1,
|
||||
"batch_eligible": True,
|
||||
"delay_minutes": 60,
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="normal",
|
||||
label="Normal",
|
||||
description="Standard priority notifications sent in regular intervals",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "minus",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"urgency_level": 2,
|
||||
"batch_eligible": True,
|
||||
"delay_minutes": 15,
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="high",
|
||||
label="High",
|
||||
description="High priority notifications sent immediately",
|
||||
metadata={
|
||||
"color": "orange",
|
||||
"icon": "arrow-up",
|
||||
"css_class": "text-orange-600 bg-orange-50",
|
||||
"urgency_level": 3,
|
||||
"batch_eligible": False,
|
||||
"delay_minutes": 0,
|
||||
"sort_order": 3,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="urgent",
|
||||
label="Urgent",
|
||||
description="Critical notifications requiring immediate attention",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "exclamation",
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"urgency_level": 4,
|
||||
"batch_eligible": False,
|
||||
"delay_minutes": 0,
|
||||
"bypass_preferences": True,
|
||||
"sort_order": 4,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# REGISTER ALL CHOICE GROUPS
|
||||
# =============================================================================
|
||||
|
||||
# Register each choice group individually
|
||||
register_choices("user_roles", user_roles.choices, "accounts", "User role classifications")
|
||||
register_choices("theme_preferences", theme_preferences.choices, "accounts", "Theme preference options")
|
||||
register_choices("privacy_levels", privacy_levels.choices, "accounts", "Privacy level settings")
|
||||
register_choices("top_list_categories", top_list_categories.choices, "accounts", "Top list category types")
|
||||
register_choices("notification_types", notification_types.choices, "accounts", "Notification type classifications")
|
||||
register_choices("notification_priorities", notification_priorities.choices, "accounts", "Notification priority levels")
|
||||
@@ -41,7 +41,7 @@ class Command(BaseCommand):
|
||||
Social auth setup instructions:
|
||||
|
||||
1. Run the development server:
|
||||
python manage.py runserver
|
||||
uv run manage.py runserver_plus
|
||||
|
||||
2. Go to the admin interface:
|
||||
http://localhost:8000/admin/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-30 20:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_avatar_data(apps, schema_editor):
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# Generated by Django 5.2.5 on 2025-09-15 17:35
|
||||
|
||||
import apps.core.choices.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0011_fix_userprofile_event_avatar_field"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="toplist",
|
||||
name="category",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="top_list_categories",
|
||||
choices=[
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("PK", "Park"),
|
||||
],
|
||||
domain="accounts",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="activity_visibility",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="privacy_levels",
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
default="friends",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="privacy_level",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="privacy_levels",
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
default="public",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="role",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="user_roles",
|
||||
choices=[
|
||||
("USER", "User"),
|
||||
("MODERATOR", "Moderator"),
|
||||
("ADMIN", "Admin"),
|
||||
("SUPERUSER", "Superuser"),
|
||||
],
|
||||
default="USER",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="theme_preference",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="theme_preferences",
|
||||
choices=[("light", "Light"), ("dark", "Dark")],
|
||||
default="light",
|
||||
domain="accounts",
|
||||
max_length=5,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="activity_visibility",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="privacy_levels",
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
default="friends",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="privacy_level",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="privacy_levels",
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
default="public",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="role",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="user_roles",
|
||||
choices=[
|
||||
("USER", "User"),
|
||||
("MODERATOR", "Moderator"),
|
||||
("ADMIN", "Admin"),
|
||||
("SUPERUSER", "Superuser"),
|
||||
],
|
||||
default="USER",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="theme_preference",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="theme_preferences",
|
||||
choices=[("light", "Light"), ("dark", "Dark")],
|
||||
default="light",
|
||||
domain="accounts",
|
||||
max_length=5,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="notification_type",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="notification_types",
|
||||
choices=[
|
||||
("submission_approved", "Submission Approved"),
|
||||
("submission_rejected", "Submission Rejected"),
|
||||
("submission_pending", "Submission Pending Review"),
|
||||
("review_reply", "Review Reply"),
|
||||
("review_helpful", "Review Marked Helpful"),
|
||||
("friend_request", "Friend Request"),
|
||||
("friend_accepted", "Friend Request Accepted"),
|
||||
("message_received", "Message Received"),
|
||||
("profile_comment", "Profile Comment"),
|
||||
("system_announcement", "System Announcement"),
|
||||
("account_security", "Account Security"),
|
||||
("feature_update", "Feature Update"),
|
||||
("maintenance", "Maintenance Notice"),
|
||||
("achievement_unlocked", "Achievement Unlocked"),
|
||||
("milestone_reached", "Milestone Reached"),
|
||||
],
|
||||
domain="accounts",
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="priority",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="notification_priorities",
|
||||
choices=[
|
||||
("low", "Low"),
|
||||
("normal", "Normal"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
],
|
||||
default="normal",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="notification_type",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="notification_types",
|
||||
choices=[
|
||||
("submission_approved", "Submission Approved"),
|
||||
("submission_rejected", "Submission Rejected"),
|
||||
("submission_pending", "Submission Pending Review"),
|
||||
("review_reply", "Review Reply"),
|
||||
("review_helpful", "Review Marked Helpful"),
|
||||
("friend_request", "Friend Request"),
|
||||
("friend_accepted", "Friend Request Accepted"),
|
||||
("message_received", "Message Received"),
|
||||
("profile_comment", "Profile Comment"),
|
||||
("system_announcement", "System Announcement"),
|
||||
("account_security", "Account Security"),
|
||||
("feature_update", "Feature Update"),
|
||||
("maintenance", "Maintenance Notice"),
|
||||
("achievement_unlocked", "Achievement Unlocked"),
|
||||
("milestone_reached", "Milestone Reached"),
|
||||
],
|
||||
domain="accounts",
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="priority",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="notification_priorities",
|
||||
choices=[
|
||||
("low", "Low"),
|
||||
("normal", "Normal"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
],
|
||||
default="normal",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ import secrets
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from apps.core.history import TrackedModel
|
||||
from apps.core.choices import RichChoiceField
|
||||
import pghistory
|
||||
|
||||
|
||||
@@ -28,21 +29,6 @@ def generate_random_id(model_class, id_field):
|
||||
|
||||
@pghistory.track()
|
||||
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")
|
||||
|
||||
class PrivacyLevel(models.TextChoices):
|
||||
PUBLIC = "public", _("Public")
|
||||
FRIENDS = "friends", _("Friends Only")
|
||||
PRIVATE = "private", _("Private")
|
||||
|
||||
# Override inherited fields to remove them
|
||||
first_name = None
|
||||
last_name = None
|
||||
@@ -58,19 +44,21 @@ class User(AbstractUser):
|
||||
),
|
||||
)
|
||||
|
||||
role = models.CharField(
|
||||
role = RichChoiceField(
|
||||
choice_group="user_roles",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
choices=Roles.choices,
|
||||
default=Roles.USER,
|
||||
default="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(
|
||||
theme_preference = RichChoiceField(
|
||||
choice_group="theme_preferences",
|
||||
domain="accounts",
|
||||
max_length=5,
|
||||
choices=ThemePreference.choices,
|
||||
default=ThemePreference.LIGHT,
|
||||
default="light",
|
||||
)
|
||||
|
||||
# Notification preferences
|
||||
@@ -78,10 +66,11 @@ class User(AbstractUser):
|
||||
push_notifications = models.BooleanField(default=False)
|
||||
|
||||
# Privacy settings
|
||||
privacy_level = models.CharField(
|
||||
privacy_level = RichChoiceField(
|
||||
choice_group="privacy_levels",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
choices=PrivacyLevel.choices,
|
||||
default=PrivacyLevel.PUBLIC,
|
||||
default="public",
|
||||
)
|
||||
show_email = models.BooleanField(default=False)
|
||||
show_real_name = models.BooleanField(default=True)
|
||||
@@ -94,10 +83,11 @@ class User(AbstractUser):
|
||||
allow_messages = models.BooleanField(default=True)
|
||||
allow_profile_comments = models.BooleanField(default=False)
|
||||
search_visibility = models.BooleanField(default=True)
|
||||
activity_visibility = models.CharField(
|
||||
activity_visibility = RichChoiceField(
|
||||
choice_group="privacy_levels",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
choices=PrivacyLevel.choices,
|
||||
default=PrivacyLevel.FRIENDS,
|
||||
default="friends",
|
||||
)
|
||||
|
||||
# Security settings
|
||||
@@ -298,20 +288,17 @@ class PasswordReset(models.Model):
|
||||
|
||||
|
||||
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)
|
||||
category = RichChoiceField(
|
||||
choice_group="top_list_categories",
|
||||
domain="accounts",
|
||||
max_length=2,
|
||||
)
|
||||
description = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -462,45 +449,15 @@ class UserNotification(TrackedModel):
|
||||
and other user-relevant notifications.
|
||||
"""
|
||||
|
||||
class NotificationType(models.TextChoices):
|
||||
# Submission related
|
||||
SUBMISSION_APPROVED = "submission_approved", _("Submission Approved")
|
||||
SUBMISSION_REJECTED = "submission_rejected", _("Submission Rejected")
|
||||
SUBMISSION_PENDING = "submission_pending", _("Submission Pending Review")
|
||||
|
||||
# Review related
|
||||
REVIEW_REPLY = "review_reply", _("Review Reply")
|
||||
REVIEW_HELPFUL = "review_helpful", _("Review Marked Helpful")
|
||||
|
||||
# Social related
|
||||
FRIEND_REQUEST = "friend_request", _("Friend Request")
|
||||
FRIEND_ACCEPTED = "friend_accepted", _("Friend Request Accepted")
|
||||
MESSAGE_RECEIVED = "message_received", _("Message Received")
|
||||
PROFILE_COMMENT = "profile_comment", _("Profile Comment")
|
||||
|
||||
# System related
|
||||
SYSTEM_ANNOUNCEMENT = "system_announcement", _("System Announcement")
|
||||
ACCOUNT_SECURITY = "account_security", _("Account Security")
|
||||
FEATURE_UPDATE = "feature_update", _("Feature Update")
|
||||
MAINTENANCE = "maintenance", _("Maintenance Notice")
|
||||
|
||||
# Achievement related
|
||||
ACHIEVEMENT_UNLOCKED = "achievement_unlocked", _("Achievement Unlocked")
|
||||
MILESTONE_REACHED = "milestone_reached", _("Milestone Reached")
|
||||
|
||||
class Priority(models.TextChoices):
|
||||
LOW = "low", _("Low")
|
||||
NORMAL = "normal", _("Normal")
|
||||
HIGH = "high", _("High")
|
||||
URGENT = "urgent", _("Urgent")
|
||||
|
||||
# Core fields
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="notifications"
|
||||
)
|
||||
|
||||
notification_type = models.CharField(
|
||||
max_length=30, choices=NotificationType.choices
|
||||
notification_type = RichChoiceField(
|
||||
choice_group="notification_types",
|
||||
domain="accounts",
|
||||
max_length=30,
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=200)
|
||||
@@ -514,8 +471,11 @@ class UserNotification(TrackedModel):
|
||||
related_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
# Metadata
|
||||
priority = models.CharField(
|
||||
max_length=10, choices=Priority.choices, default=Priority.NORMAL
|
||||
priority = RichChoiceField(
|
||||
choice_group="notification_priorities",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
default="normal",
|
||||
)
|
||||
|
||||
# Status tracking
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
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 apps.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[0].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}"
|
||||
Reference in New Issue
Block a user