diff --git a/accounts/__pycache__/admin.cpython-312.pyc b/accounts/__pycache__/admin.cpython-312.pyc
index 3ee27ffb..d71a5097 100644
Binary files a/accounts/__pycache__/admin.cpython-312.pyc and b/accounts/__pycache__/admin.cpython-312.pyc differ
diff --git a/accounts/__pycache__/models.cpython-312.pyc b/accounts/__pycache__/models.cpython-312.pyc
index 54f35abd..f543d811 100644
Binary files a/accounts/__pycache__/models.cpython-312.pyc and b/accounts/__pycache__/models.cpython-312.pyc differ
diff --git a/accounts/__pycache__/views.cpython-312.pyc b/accounts/__pycache__/views.cpython-312.pyc
index 3f1bcf4e..afda2d42 100644
Binary files a/accounts/__pycache__/views.cpython-312.pyc and b/accounts/__pycache__/views.cpython-312.pyc differ
diff --git a/accounts/admin.py b/accounts/admin.py
index 034ca678..d8ecc3e1 100644
--- a/accounts/admin.py
+++ b/accounts/admin.py
@@ -34,16 +34,16 @@ class TopListItemInline(admin.TabularInline):
@admin.register(User)
class CustomUserAdmin(UserAdmin):
- list_display = ('username', 'email', 'get_status', 'role', 'date_joined', 'last_login', 'get_credits')
+ list_display = ('username', 'email', 'get_avatar', 'get_status', 'role', 'date_joined', 'last_login', 'get_credits')
list_filter = ('is_active', 'is_staff', 'role', 'is_banned', 'groups', 'date_joined')
- search_fields = ('username', 'email', 'first_name', 'last_name')
+ search_fields = ('username', 'email')
ordering = ('-date_joined',)
actions = ['activate_users', 'deactivate_users', 'ban_users', 'unban_users']
inlines = [UserProfileInline]
fieldsets = (
(None, {'fields': ('username', 'password')}),
- ('Personal info', {'fields': ('first_name', 'last_name', 'email', 'pending_email')}),
+ ('Personal info', {'fields': ('email', 'pending_email')}),
('Roles and Permissions', {
'fields': ('role', 'groups', 'user_permissions'),
'description': 'Role determines group membership. Groups determine permissions.',
@@ -67,6 +67,12 @@ class CustomUserAdmin(UserAdmin):
}),
)
+ def get_avatar(self, obj):
+ if obj.profile.avatar:
+ return format_html('
', obj.profile.avatar.url)
+ return format_html('
{}
', obj.username[0].upper())
+ get_avatar.short_description = 'Avatar'
+
def get_status(self, obj):
if obj.is_banned:
return format_html('Banned')
diff --git a/accounts/management/commands/generate_letter_avatars.py b/accounts/management/commands/generate_letter_avatars.py
new file mode 100644
index 00000000..922e2c8c
--- /dev/null
+++ b/accounts/management/commands/generate_letter_avatars.py
@@ -0,0 +1,44 @@
+from django.core.management.base import BaseCommand
+from PIL import Image, ImageDraw, ImageFont
+import os
+
+def generate_avatar(letter):
+ """Generate an avatar for a given letter or number"""
+ avatar_size = (100, 100)
+ background_color = (0, 123, 255) # Blue background
+ text_color = (255, 255, 255) # White text
+ font_size = 100
+
+ # Create a blank image with background color
+ image = Image.new('RGB', avatar_size, background_color)
+ draw = ImageDraw.Draw(image)
+
+ # Load a font
+ font_path = "[AWS-SECRET-REMOVED]ans-Bold.ttf"
+ font = ImageFont.truetype(font_path, font_size)
+
+ # Calculate text size and position using textbbox
+ text_bbox = draw.textbbox((0, 0), letter, font=font)
+ text_width, text_height = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
+ text_position = ((avatar_size[0] - text_width) / 2, (avatar_size[1] - text_height) / 2)
+
+ # Draw the text on the image
+ draw.text(text_position, letter, font=font, fill=text_color)
+
+ # Ensure the avatars directory exists
+ avatar_dir = "avatars/letters"
+ if not os.path.exists(avatar_dir):
+ os.makedirs(avatar_dir)
+
+ # Save the image to the avatars directory
+ avatar_path = os.path.join(avatar_dir, f"{letter}_avatar.png")
+ image.save(avatar_path)
+
+class Command(BaseCommand):
+ help = 'Generate avatars for letters A-Z and numbers 0-9'
+
+ def handle(self, *args, **kwargs):
+ characters = [chr(i) for i in range(65, 91)] + [str(i) for i in range(10)] # A-Z and 0-9
+ for char in characters:
+ generate_avatar(char)
+ self.stdout.write(self.style.SUCCESS(f"Generated avatar for {char}"))
diff --git a/accounts/management/commands/regenerate_avatars.py b/accounts/management/commands/regenerate_avatars.py
new file mode 100644
index 00000000..fcb94248
--- /dev/null
+++ b/accounts/management/commands/regenerate_avatars.py
@@ -0,0 +1,11 @@
+from django.core.management.base import BaseCommand
+from accounts.models import UserProfile
+
+class Command(BaseCommand):
+ help = 'Regenerate default avatars for users without an uploaded avatar'
+
+ def handle(self, *args, **kwargs):
+ profiles = UserProfile.objects.filter(avatar='')
+ for profile in profiles:
+ profile.save() # This will trigger the avatar generation logic in the save method
+ self.stdout.write(self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}"))
diff --git a/accounts/models.py b/accounts/models.py
index 312f357b..5f5072d6 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -3,6 +3,10 @@ from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import random
+from PIL import Image, ImageDraw, ImageFont
+from io import BytesIO
+import base64
+import os
def generate_random_id(model_class, id_field):
"""Generate a random ID starting at 4 digits, expanding to 5 if needed"""
@@ -36,8 +40,6 @@ class User(AbstractUser):
help_text='Unique identifier for this user that remains constant even if the username changes'
)
- first_name = models.CharField(_('first name'), max_length=150, default='')
- last_name = models.CharField(_('last name'), max_length=150, default='')
role = models.CharField(
max_length=10,
choices=Roles.choices,
@@ -61,8 +63,9 @@ class User(AbstractUser):
def get_display_name(self):
"""Get the user's display name, falling back to username if not set"""
- if hasattr(self, 'profile') and self.profile.display_name:
- return self.profile.display_name
+ profile = getattr(self, 'profile', None)
+ if profile and profile.display_name:
+ return profile.display_name
return self.username
def save(self, *args, **kwargs):
@@ -106,10 +109,21 @@ class UserProfile(models.Model):
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)
@@ -155,7 +169,7 @@ class TopList(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
- related_name='top_lists'
+ related_name='top_lists' # Added related_name for User model access
)
title = models.CharField(max_length=100)
category = models.CharField(
@@ -170,7 +184,7 @@ class TopList(models.Model):
ordering = ['-updated_at']
def __str__(self):
- return f"{self.user.get_display_name()}'s {self.get_category_display()} Top List: {self.title}"
+ return f"{self.user.get_display_name()}'s {self.category} Top List: {self.title}"
class TopListItem(models.Model):
top_list = models.ForeignKey(
diff --git a/accounts/views.py b/accounts/views.py
index 2bfa22b2..95a75335 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -18,7 +18,7 @@ from django.contrib.sites.shortcuts import get_current_site
from django.db.models import Prefetch
from django.http import HttpResponseRedirect
from django.urls import reverse
-from accounts.models import User, PasswordReset
+from accounts.models import User, PasswordReset, TopList, EmailVerification
from reviews.models import Review
from email_service.services import EmailService
from allauth.account.views import LoginView, SignupView
@@ -101,16 +101,8 @@ class ProfileView(DetailView):
context['recent_reviews'] = reviews_queryset
# Get user's top lists with optimized queries
- context['top_lists'] = user.top_lists.select_related(
- 'user',
- 'user__profile'
- ).prefetch_related(
- Prefetch('items', queryset=(
- user.top_lists.through.objects.select_related(
- 'content_type'
- ).prefetch_related('content_object')
- ))
- ).order_by('-created_at')[:5]
+ top_lists_queryset = TopList.objects.filter(user=user).select_related('user', 'user__profile').prefetch_related('items')
+ context['top_lists'] = top_lists_queryset.order_by('-created_at')[:5]
return context
@@ -128,8 +120,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
if action == 'update_profile':
# Handle profile updates
user = request.user
- user.first_name = request.POST.get('first_name', user.first_name)
- user.last_name = request.POST.get('last_name', user.last_name)
+ user.profile.display_name = request.POST.get('display_name', user.profile.display_name)
if 'avatar' in request.FILES:
user.profile.avatar = request.FILES['avatar']
@@ -150,7 +141,37 @@ class SettingsView(LoginRequiredMixin, TemplateView):
return HttpResponseRedirect(reverse('account_login'))
else:
messages.error(request, 'Current password is incorrect')
-
+
+ elif action == 'change_email':
+ # Handle email change with verification
+ new_email = request.POST.get('new_email')
+ if new_email:
+ token = get_random_string(64)
+ EmailVerification.objects.update_or_create(
+ user=request.user,
+ defaults={'token': token}
+ )
+ site = get_current_site(request)
+ verification_url = reverse('verify_email', kwargs={'token': token})
+ context = {
+ 'user': request.user,
+ 'verification_url': verification_url,
+ 'site_name': site.name,
+ }
+ email_html = render_to_string('accounts/email/verify_email.html', context)
+ EmailService.send_email(
+ to=new_email,
+ subject='Verify your new email address',
+ text='Click the link to verify your new email address',
+ site=site,
+ html=email_html
+ )
+ request.user.pending_email = new_email
+ request.user.save()
+ messages.success(request, 'Verification email sent to your new email address')
+ else:
+ messages.error(request, 'New email is required')
+
return self.get(request, *args, **kwargs)
def request_password_reset(request):
diff --git a/avatars/letters/0_avatar.png b/avatars/letters/0_avatar.png
new file mode 100644
index 00000000..386a9c5a
Binary files /dev/null and b/avatars/letters/0_avatar.png differ
diff --git a/avatars/letters/1_avatar.png b/avatars/letters/1_avatar.png
new file mode 100644
index 00000000..bf69a3ed
Binary files /dev/null and b/avatars/letters/1_avatar.png differ
diff --git a/avatars/letters/2_avatar.png b/avatars/letters/2_avatar.png
new file mode 100644
index 00000000..992aba8e
Binary files /dev/null and b/avatars/letters/2_avatar.png differ
diff --git a/avatars/letters/3_avatar.png b/avatars/letters/3_avatar.png
new file mode 100644
index 00000000..8b4acc8e
Binary files /dev/null and b/avatars/letters/3_avatar.png differ
diff --git a/avatars/letters/4_avatar.png b/avatars/letters/4_avatar.png
new file mode 100644
index 00000000..db8a5fdb
Binary files /dev/null and b/avatars/letters/4_avatar.png differ
diff --git a/avatars/letters/5_avatar.png b/avatars/letters/5_avatar.png
new file mode 100644
index 00000000..720bfb36
Binary files /dev/null and b/avatars/letters/5_avatar.png differ
diff --git a/avatars/letters/6_avatar.png b/avatars/letters/6_avatar.png
new file mode 100644
index 00000000..ff823d82
Binary files /dev/null and b/avatars/letters/6_avatar.png differ
diff --git a/avatars/letters/7_avatar.png b/avatars/letters/7_avatar.png
new file mode 100644
index 00000000..d70771f6
Binary files /dev/null and b/avatars/letters/7_avatar.png differ
diff --git a/avatars/letters/8_avatar.png b/avatars/letters/8_avatar.png
new file mode 100644
index 00000000..cc7e1dbc
Binary files /dev/null and b/avatars/letters/8_avatar.png differ
diff --git a/avatars/letters/9_avatar.png b/avatars/letters/9_avatar.png
new file mode 100644
index 00000000..6fde44c8
Binary files /dev/null and b/avatars/letters/9_avatar.png differ
diff --git a/avatars/letters/A_avatar.png b/avatars/letters/A_avatar.png
new file mode 100644
index 00000000..b18da02c
Binary files /dev/null and b/avatars/letters/A_avatar.png differ
diff --git a/avatars/letters/B_avatar.png b/avatars/letters/B_avatar.png
new file mode 100644
index 00000000..d66ea1b7
Binary files /dev/null and b/avatars/letters/B_avatar.png differ
diff --git a/avatars/letters/C_avatar.png b/avatars/letters/C_avatar.png
new file mode 100644
index 00000000..e0ccb580
Binary files /dev/null and b/avatars/letters/C_avatar.png differ
diff --git a/avatars/letters/D_avatar.png b/avatars/letters/D_avatar.png
new file mode 100644
index 00000000..7dd34961
Binary files /dev/null and b/avatars/letters/D_avatar.png differ
diff --git a/avatars/letters/E_avatar.png b/avatars/letters/E_avatar.png
new file mode 100644
index 00000000..46626957
Binary files /dev/null and b/avatars/letters/E_avatar.png differ
diff --git a/avatars/letters/F_avatar.png b/avatars/letters/F_avatar.png
new file mode 100644
index 00000000..adfbfc0f
Binary files /dev/null and b/avatars/letters/F_avatar.png differ
diff --git a/avatars/letters/G_avatar.png b/avatars/letters/G_avatar.png
new file mode 100644
index 00000000..84fa26c5
Binary files /dev/null and b/avatars/letters/G_avatar.png differ
diff --git a/avatars/letters/H_avatar.png b/avatars/letters/H_avatar.png
new file mode 100644
index 00000000..d0847b2d
Binary files /dev/null and b/avatars/letters/H_avatar.png differ
diff --git a/avatars/letters/I_avatar.png b/avatars/letters/I_avatar.png
new file mode 100644
index 00000000..0fb6102b
Binary files /dev/null and b/avatars/letters/I_avatar.png differ
diff --git a/avatars/letters/J_avatar.png b/avatars/letters/J_avatar.png
new file mode 100644
index 00000000..f6bb2945
Binary files /dev/null and b/avatars/letters/J_avatar.png differ
diff --git a/avatars/letters/K_avatar.png b/avatars/letters/K_avatar.png
new file mode 100644
index 00000000..5a128787
Binary files /dev/null and b/avatars/letters/K_avatar.png differ
diff --git a/avatars/letters/L_avatar.png b/avatars/letters/L_avatar.png
new file mode 100644
index 00000000..c32f6dac
Binary files /dev/null and b/avatars/letters/L_avatar.png differ
diff --git a/avatars/letters/M_avatar.png b/avatars/letters/M_avatar.png
new file mode 100644
index 00000000..3a41bfbf
Binary files /dev/null and b/avatars/letters/M_avatar.png differ
diff --git a/avatars/letters/N_avatar.png b/avatars/letters/N_avatar.png
new file mode 100644
index 00000000..63134b10
Binary files /dev/null and b/avatars/letters/N_avatar.png differ
diff --git a/avatars/letters/O_avatar.png b/avatars/letters/O_avatar.png
new file mode 100644
index 00000000..1327bf3d
Binary files /dev/null and b/avatars/letters/O_avatar.png differ
diff --git a/avatars/letters/P_avatar.png b/avatars/letters/P_avatar.png
new file mode 100644
index 00000000..b0ba5843
Binary files /dev/null and b/avatars/letters/P_avatar.png differ
diff --git a/avatars/letters/Q_avatar.png b/avatars/letters/Q_avatar.png
new file mode 100644
index 00000000..d15ca407
Binary files /dev/null and b/avatars/letters/Q_avatar.png differ
diff --git a/avatars/letters/R_avatar.png b/avatars/letters/R_avatar.png
new file mode 100644
index 00000000..9f68b774
Binary files /dev/null and b/avatars/letters/R_avatar.png differ
diff --git a/avatars/letters/S_avatar.png b/avatars/letters/S_avatar.png
new file mode 100644
index 00000000..9dc10694
Binary files /dev/null and b/avatars/letters/S_avatar.png differ
diff --git a/avatars/letters/T_avatar.png b/avatars/letters/T_avatar.png
new file mode 100644
index 00000000..53af421e
Binary files /dev/null and b/avatars/letters/T_avatar.png differ
diff --git a/avatars/letters/U_avatar.png b/avatars/letters/U_avatar.png
new file mode 100644
index 00000000..50914422
Binary files /dev/null and b/avatars/letters/U_avatar.png differ
diff --git a/avatars/letters/V_avatar.png b/avatars/letters/V_avatar.png
new file mode 100644
index 00000000..eca19b33
Binary files /dev/null and b/avatars/letters/V_avatar.png differ
diff --git a/avatars/letters/W_avatar.png b/avatars/letters/W_avatar.png
new file mode 100644
index 00000000..1f8ab220
Binary files /dev/null and b/avatars/letters/W_avatar.png differ
diff --git a/avatars/letters/X_avatar.png b/avatars/letters/X_avatar.png
new file mode 100644
index 00000000..ce50a79d
Binary files /dev/null and b/avatars/letters/X_avatar.png differ
diff --git a/avatars/letters/Y_avatar.png b/avatars/letters/Y_avatar.png
new file mode 100644
index 00000000..7bf3d7b6
Binary files /dev/null and b/avatars/letters/Y_avatar.png differ
diff --git a/avatars/letters/Z_avatar.png b/avatars/letters/Z_avatar.png
new file mode 100644
index 00000000..cad23538
Binary files /dev/null and b/avatars/letters/Z_avatar.png differ
diff --git a/media/avatars/loopy.png b/media/avatars/loopy.png
new file mode 100644
index 00000000..fbcebfae
Binary files /dev/null and b/media/avatars/loopy.png differ
diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html
index 98bf2b17..36de5fab 100644
--- a/templates/accounts/profile.html
+++ b/templates/accounts/profile.html
@@ -4,14 +4,20 @@
{% block title %}{{ profile_user.username }}'s Profile - ThrillWiki{% endblock %}
{% block content %}
-
+
-
+
-

+ class="object-cover w-24 h-24 rounded-full">
+ {% else %}
+
+ {{ profile_user.username.0|upper }}
+
+ {% endif %}
{{ profile_user.profile.display_name|default:profile_user.username }}
@@ -31,14 +37,14 @@
{% if profile_user.profile.bio %}
-
About Me
+
About Me
{{ profile_user.profile.bio }}
{% endif %}
{% if profile_user.profile.twitter or profile_user.profile.instagram or profile_user.profile.youtube or profile_user.profile.discord %}
-
+
-
+
-
+
{{ profile_user.profile.coaster_credits }}
Coaster Credits
-
+
{{ profile_user.profile.dark_ride_credits }}
Dark Ride Credits
-
+
{{ profile_user.profile.flat_ride_credits }}
Flat Ride Credits
-
+
{{ profile_user.profile.water_ride_credits }}
Water Ride Credits
@@ -96,7 +102,7 @@
-