mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 18:11:09 -05:00
profile changes
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>')
|
||||
|
||||
44
accounts/management/commands/generate_letter_avatars.py
Normal file
44
accounts/management/commands/generate_letter_avatars.py
Normal 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 = "/usr/share/fonts/truetype/dejavu/DejaVuSans-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}"))
|
||||
11
accounts/management/commands/regenerate_avatars.py
Normal file
11
accounts/management/commands/regenerate_avatars.py
Normal 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}"))
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user