mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
- 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.
585 lines
17 KiB
Python
585 lines
17 KiB
Python
"""
|
|
Django admin configuration for User models.
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
from django.utils.html import format_html
|
|
from django.urls import reverse
|
|
from django.utils.safestring import mark_safe
|
|
from unfold.admin import ModelAdmin
|
|
from unfold.decorators import display
|
|
from import_export import resources
|
|
from import_export.admin import ImportExportModelAdmin
|
|
|
|
from .models import User, UserRole, UserProfile, UserRideCredit, UserTopList, UserTopListItem
|
|
|
|
|
|
class UserResource(resources.ModelResource):
|
|
"""Resource for importing/exporting users."""
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = (
|
|
'id', 'email', 'username', 'first_name', 'last_name',
|
|
'date_joined', 'last_login', 'is_active', 'is_staff',
|
|
'banned', 'reputation_score', 'mfa_enabled'
|
|
)
|
|
export_order = fields
|
|
|
|
|
|
class UserRoleInline(admin.StackedInline):
|
|
"""Inline for user role."""
|
|
model = UserRole
|
|
can_delete = False
|
|
verbose_name_plural = 'Role'
|
|
fk_name = 'user'
|
|
fields = ('role', 'granted_by', 'granted_at')
|
|
readonly_fields = ('granted_at',)
|
|
|
|
|
|
class UserProfileInline(admin.StackedInline):
|
|
"""Inline for user profile."""
|
|
model = UserProfile
|
|
can_delete = False
|
|
verbose_name_plural = 'Profile & Preferences'
|
|
fk_name = 'user'
|
|
fields = (
|
|
('email_notifications', 'email_on_submission_approved', 'email_on_submission_rejected'),
|
|
('profile_public', 'show_email'),
|
|
('total_submissions', 'approved_submissions'),
|
|
)
|
|
readonly_fields = ('total_submissions', 'approved_submissions')
|
|
|
|
|
|
@admin.register(User)
|
|
class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
"""Admin interface for User model."""
|
|
|
|
resource_class = UserResource
|
|
|
|
list_display = [
|
|
'email',
|
|
'username',
|
|
'display_name_admin',
|
|
'role_badge',
|
|
'reputation_badge',
|
|
'status_badge',
|
|
'mfa_badge',
|
|
'date_joined',
|
|
'last_login',
|
|
]
|
|
|
|
list_filter = [
|
|
'is_active',
|
|
'is_staff',
|
|
'is_superuser',
|
|
'banned',
|
|
'mfa_enabled',
|
|
'oauth_provider',
|
|
'date_joined',
|
|
'last_login',
|
|
]
|
|
|
|
search_fields = [
|
|
'email',
|
|
'username',
|
|
'first_name',
|
|
'last_name',
|
|
]
|
|
|
|
ordering = ['-date_joined']
|
|
|
|
fieldsets = (
|
|
('Account Information', {
|
|
'fields': ('email', 'username', 'password')
|
|
}),
|
|
('Personal Information', {
|
|
'fields': ('first_name', 'last_name', 'avatar_url', 'bio')
|
|
}),
|
|
('Permissions', {
|
|
'fields': (
|
|
'is_active',
|
|
'is_staff',
|
|
'is_superuser',
|
|
'groups',
|
|
'user_permissions',
|
|
)
|
|
}),
|
|
('Moderation', {
|
|
'fields': (
|
|
'banned',
|
|
'ban_reason',
|
|
'banned_at',
|
|
'banned_by',
|
|
)
|
|
}),
|
|
('OAuth', {
|
|
'fields': ('oauth_provider', 'oauth_sub'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Security', {
|
|
'fields': ('mfa_enabled', 'reputation_score'),
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('date_joined', 'last_login'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
add_fieldsets = (
|
|
('Create New User', {
|
|
'classes': ('wide',),
|
|
'fields': ('email', 'username', 'password1', 'password2'),
|
|
}),
|
|
)
|
|
|
|
readonly_fields = [
|
|
'date_joined',
|
|
'last_login',
|
|
'banned_at',
|
|
'oauth_provider',
|
|
'oauth_sub',
|
|
]
|
|
|
|
inlines = [UserRoleInline, UserProfileInline]
|
|
|
|
@display(description="Name", label=True)
|
|
def display_name_admin(self, obj):
|
|
"""Display user's display name."""
|
|
return obj.display_name or '-'
|
|
|
|
@display(description="Role", label=True)
|
|
def role_badge(self, obj):
|
|
"""Display user role with badge."""
|
|
try:
|
|
role = obj.role.role
|
|
colors = {
|
|
'admin': 'red',
|
|
'moderator': 'blue',
|
|
'user': 'green',
|
|
}
|
|
return format_html(
|
|
'<span style="background-color: {}; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">{}</span>',
|
|
colors.get(role, 'gray'),
|
|
role.upper()
|
|
)
|
|
except UserRole.DoesNotExist:
|
|
return format_html('<span style="color: gray;">No Role</span>')
|
|
|
|
@display(description="Reputation", label=True)
|
|
def reputation_badge(self, obj):
|
|
"""Display reputation score."""
|
|
score = obj.reputation_score
|
|
if score >= 100:
|
|
color = 'green'
|
|
elif score >= 50:
|
|
color = 'blue'
|
|
elif score >= 0:
|
|
color = 'gray'
|
|
else:
|
|
color = 'red'
|
|
|
|
return format_html(
|
|
'<span style="color: {}; font-weight: bold;">{}</span>',
|
|
color,
|
|
score
|
|
)
|
|
|
|
@display(description="Status", label=True)
|
|
def status_badge(self, obj):
|
|
"""Display user status."""
|
|
if obj.banned:
|
|
return format_html(
|
|
'<span style="background-color: red; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">BANNED</span>'
|
|
)
|
|
elif not obj.is_active:
|
|
return format_html(
|
|
'<span style="background-color: orange; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">INACTIVE</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="background-color: green; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">ACTIVE</span>'
|
|
)
|
|
|
|
@display(description="MFA", label=True)
|
|
def mfa_badge(self, obj):
|
|
"""Display MFA status."""
|
|
if obj.mfa_enabled:
|
|
return format_html(
|
|
'<span style="color: green;">✓ Enabled</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="color: gray;">✗ Disabled</span>'
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
"""Optimize queryset with select_related."""
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('role', 'banned_by')
|
|
|
|
actions = ['ban_users', 'unban_users', 'make_moderator', 'make_user']
|
|
|
|
@admin.action(description="Ban selected users")
|
|
def ban_users(self, request, queryset):
|
|
"""Ban selected users."""
|
|
count = 0
|
|
for user in queryset:
|
|
if not user.banned:
|
|
user.ban(reason="Banned by admin", banned_by=request.user)
|
|
count += 1
|
|
|
|
self.message_user(
|
|
request,
|
|
f"{count} user(s) have been banned."
|
|
)
|
|
|
|
@admin.action(description="Unban selected users")
|
|
def unban_users(self, request, queryset):
|
|
"""Unban selected users."""
|
|
count = 0
|
|
for user in queryset:
|
|
if user.banned:
|
|
user.unban()
|
|
count += 1
|
|
|
|
self.message_user(
|
|
request,
|
|
f"{count} user(s) have been unbanned."
|
|
)
|
|
|
|
@admin.action(description="Set role to Moderator")
|
|
def make_moderator(self, request, queryset):
|
|
"""Set users' role to moderator."""
|
|
from .services import RoleService
|
|
|
|
count = 0
|
|
for user in queryset:
|
|
RoleService.assign_role(user, 'moderator', request.user)
|
|
count += 1
|
|
|
|
self.message_user(
|
|
request,
|
|
f"{count} user(s) have been set to Moderator role."
|
|
)
|
|
|
|
@admin.action(description="Set role to User")
|
|
def make_user(self, request, queryset):
|
|
"""Set users' role to user."""
|
|
from .services import RoleService
|
|
|
|
count = 0
|
|
for user in queryset:
|
|
RoleService.assign_role(user, 'user', request.user)
|
|
count += 1
|
|
|
|
self.message_user(
|
|
request,
|
|
f"{count} user(s) have been set to User role."
|
|
)
|
|
|
|
|
|
@admin.register(UserRole)
|
|
class UserRoleAdmin(ModelAdmin):
|
|
"""Admin interface for UserRole model."""
|
|
|
|
list_display = ['user', 'role', 'is_moderator', 'is_admin', 'granted_at', 'granted_by']
|
|
list_filter = ['role', 'granted_at']
|
|
search_fields = ['user__email', 'user__username']
|
|
ordering = ['-granted_at']
|
|
|
|
readonly_fields = ['granted_at']
|
|
|
|
def get_queryset(self, request):
|
|
"""Optimize queryset."""
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('user', 'granted_by')
|
|
|
|
|
|
@admin.register(UserProfile)
|
|
class UserProfileAdmin(ModelAdmin):
|
|
"""Admin interface for UserProfile model."""
|
|
|
|
list_display = [
|
|
'user',
|
|
'total_submissions',
|
|
'approved_submissions',
|
|
'approval_rate',
|
|
'email_notifications',
|
|
'profile_public',
|
|
]
|
|
|
|
list_filter = [
|
|
'email_notifications',
|
|
'profile_public',
|
|
'show_email',
|
|
]
|
|
|
|
search_fields = ['user__email', 'user__username']
|
|
|
|
readonly_fields = ['created', 'modified', 'total_submissions', 'approved_submissions']
|
|
|
|
fieldsets = (
|
|
('User', {
|
|
'fields': ('user',)
|
|
}),
|
|
('Statistics', {
|
|
'fields': ('total_submissions', 'approved_submissions'),
|
|
}),
|
|
('Notification Preferences', {
|
|
'fields': (
|
|
'email_notifications',
|
|
'email_on_submission_approved',
|
|
'email_on_submission_rejected',
|
|
)
|
|
}),
|
|
('Privacy Settings', {
|
|
'fields': ('profile_public', 'show_email'),
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created', 'modified'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
@display(description="Approval Rate")
|
|
def approval_rate(self, obj):
|
|
"""Display approval rate percentage."""
|
|
if obj.total_submissions == 0:
|
|
return '-'
|
|
|
|
rate = (obj.approved_submissions / obj.total_submissions) * 100
|
|
|
|
if rate >= 80:
|
|
color = 'green'
|
|
elif rate >= 60:
|
|
color = 'blue'
|
|
elif rate >= 40:
|
|
color = 'orange'
|
|
else:
|
|
color = 'red'
|
|
|
|
return format_html(
|
|
'<span style="color: {}; font-weight: bold;">{:.1f}%</span>',
|
|
color,
|
|
rate
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
"""Optimize queryset."""
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('user')
|
|
|
|
|
|
@admin.register(UserRideCredit)
|
|
class UserRideCreditAdmin(ModelAdmin):
|
|
"""Admin interface for UserRideCredit model."""
|
|
|
|
list_display = [
|
|
'user_link',
|
|
'ride_link',
|
|
'park_link',
|
|
'first_ride_date',
|
|
'ride_count',
|
|
'created',
|
|
]
|
|
|
|
list_filter = [
|
|
'first_ride_date',
|
|
'created',
|
|
]
|
|
|
|
search_fields = [
|
|
'user__email',
|
|
'user__username',
|
|
'ride__name',
|
|
'notes',
|
|
]
|
|
|
|
ordering = ['-first_ride_date', '-created']
|
|
|
|
readonly_fields = ['created', 'modified']
|
|
|
|
fieldsets = (
|
|
('Credit Information', {
|
|
'fields': ('user', 'ride', 'first_ride_date', 'ride_count')
|
|
}),
|
|
('Notes', {
|
|
'fields': ('notes',)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created', 'modified'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
@display(description='User', ordering='user__username')
|
|
def user_link(self, obj):
|
|
url = reverse('admin:users_user_change', args=[obj.user.pk])
|
|
return format_html('<a href="{}">{}</a>', url, obj.user.username)
|
|
|
|
@display(description='Ride', ordering='ride__name')
|
|
def ride_link(self, obj):
|
|
url = reverse('admin:entities_ride_change', args=[obj.ride.pk])
|
|
return format_html('<a href="{}">{}</a>', url, obj.ride.name)
|
|
|
|
@display(description='Park')
|
|
def park_link(self, obj):
|
|
if obj.ride.park:
|
|
url = reverse('admin:entities_park_change', args=[obj.ride.park.pk])
|
|
return format_html('<a href="{}">{}</a>', url, obj.ride.park.name)
|
|
return '-'
|
|
|
|
def get_queryset(self, request):
|
|
"""Optimize queryset."""
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('user', 'ride', 'ride__park')
|
|
|
|
|
|
class UserTopListItemInline(admin.TabularInline):
|
|
"""Inline for top list items."""
|
|
model = UserTopListItem
|
|
extra = 1
|
|
fields = ('position', 'content_type', 'object_id', 'notes')
|
|
ordering = ['position']
|
|
|
|
|
|
@admin.register(UserTopList)
|
|
class UserTopListAdmin(ModelAdmin):
|
|
"""Admin interface for UserTopList model."""
|
|
|
|
list_display = [
|
|
'title',
|
|
'user_link',
|
|
'list_type',
|
|
'item_count_display',
|
|
'visibility_badge',
|
|
'created',
|
|
]
|
|
|
|
list_filter = [
|
|
'list_type',
|
|
'is_public',
|
|
'created',
|
|
]
|
|
|
|
search_fields = [
|
|
'title',
|
|
'description',
|
|
'user__email',
|
|
'user__username',
|
|
]
|
|
|
|
ordering = ['-created']
|
|
|
|
readonly_fields = ['created', 'modified', 'item_count']
|
|
|
|
fieldsets = (
|
|
('List Information', {
|
|
'fields': ('user', 'list_type', 'title', 'description')
|
|
}),
|
|
('Privacy', {
|
|
'fields': ('is_public',)
|
|
}),
|
|
('Statistics', {
|
|
'fields': ('item_count',)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created', 'modified'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
inlines = [UserTopListItemInline]
|
|
|
|
@display(description='User', ordering='user__username')
|
|
def user_link(self, obj):
|
|
url = reverse('admin:users_user_change', args=[obj.user.pk])
|
|
return format_html('<a href="{}">{}</a>', url, obj.user.username)
|
|
|
|
@display(description='Items', ordering='items__count')
|
|
def item_count_display(self, obj):
|
|
count = obj.item_count
|
|
return format_html('<span style="font-weight: bold;">{}</span>', count)
|
|
|
|
@display(description='Visibility', ordering='is_public')
|
|
def visibility_badge(self, obj):
|
|
if obj.is_public:
|
|
return format_html(
|
|
'<span style="background-color: green; color: white; padding: 3px 8px; '
|
|
'border-radius: 3px; font-size: 11px;">PUBLIC</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="background-color: gray; color: white; padding: 3px 8px; '
|
|
'border-radius: 3px; font-size: 11px;">PRIVATE</span>'
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
"""Optimize queryset."""
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('user').prefetch_related('items')
|
|
|
|
|
|
@admin.register(UserTopListItem)
|
|
class UserTopListItemAdmin(ModelAdmin):
|
|
"""Admin interface for UserTopListItem model."""
|
|
|
|
list_display = [
|
|
'position',
|
|
'list_link',
|
|
'entity_type',
|
|
'entity_link',
|
|
'created',
|
|
]
|
|
|
|
list_filter = [
|
|
'content_type',
|
|
'created',
|
|
]
|
|
|
|
search_fields = [
|
|
'top_list__title',
|
|
'notes',
|
|
]
|
|
|
|
ordering = ['top_list', 'position']
|
|
|
|
readonly_fields = ['created', 'modified']
|
|
|
|
fieldsets = (
|
|
('Item Information', {
|
|
'fields': ('top_list', 'position', 'content_type', 'object_id')
|
|
}),
|
|
('Notes', {
|
|
'fields': ('notes',)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created', 'modified'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
@display(description='List', ordering='top_list__title')
|
|
def list_link(self, obj):
|
|
url = reverse('admin:users_usertoplist_change', args=[obj.top_list.pk])
|
|
return format_html('<a href="{}">{}</a>', url, obj.top_list.title)
|
|
|
|
@display(description='Type', ordering='content_type')
|
|
def entity_type(self, obj):
|
|
return obj.content_type.model.title()
|
|
|
|
@display(description='Entity')
|
|
def entity_link(self, obj):
|
|
if obj.content_object:
|
|
model_name = obj.content_type.model
|
|
url = reverse(f'admin:entities_{model_name}_change', args=[obj.object_id])
|
|
return format_html('<a href="{}">{}</a>', url, str(obj.content_object))
|
|
return f"ID: {obj.object_id}"
|
|
|
|
def get_queryset(self, request):
|
|
"""Optimize queryset."""
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('top_list', 'content_type')
|