profile changes

This commit is contained in:
pacnpal
2024-11-05 23:36:50 +00:00
parent eb5d2acab5
commit 33b7c1a81a
47 changed files with 207 additions and 37 deletions

View File

@@ -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('<img src="{}" width="30" height="30" style="border-radius:50%;" />', obj.profile.avatar.url)
return format_html('<div style="width:30px; height:30px; border-radius:50%; background-color:#007bff; color:white; display:flex; align-items:center; justify-content:center;">{}</div>', obj.username[0].upper())
get_avatar.short_description = 'Avatar'
def get_status(self, obj):
if obj.is_banned:
return format_html('<span style="color: red;">Banned</span>')

View File

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

View File

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

View File

@@ -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(

View File

@@ -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):