updated user features and added palette

This commit is contained in:
nvrl committed 2026-02-16 00:14:28 +01:00
1 parent 03adde8928
commit 6b26b77e0e
11 files changed
+515 -140

No files matched your search

@@ -35,6 +35,7 @@ class Command(BaseCommand):
'hydration': Category.objects.get_or_create(name='Hydration', defaults={'slug': 'hydration'})[0],
'iced-tea': Category.objects.get_or_create(name='Iced Tea', defaults={'slug': 'iced-tea'})[0],
'milkshake': Category.objects.get_or_create(name='Milkshake', defaults={'slug': 'milkshake'})[0],
'packs': Category.objects.get_or_create(name='Packs and other', defaults={'slug': 'packs-and-other'})[0],
}
created_count = 0
@@ -43,14 +44,30 @@ class Command(BaseCommand):
for p in products:
title = p.get('title')
tags = p.get('tags', [])
product_type = p.get('product_type', '').lower()
if isinstance(tags, str):
tags = [t.strip() for t in tags.split(',')]
# Determine category
category = None
tags_lower = [t.lower() for t in tags]
title_lower = title.lower()
# Check for Packs/Bundles/Samples first
is_pack = False
pack_keywords = ['bundle', 'pack', 'set', 'box', 'probe', 'sample', 'taster', 'shaker', 'starter']
if 'holy energy' in tags_lower or 'energy' in tags_lower:
# Check title keywords
if any(k in title_lower for k in pack_keywords):
is_pack = True
# Check product type keywords
if any(k in product_type for k in pack_keywords):
is_pack = True
if is_pack:
category = cat_map['packs']
elif 'holy energy' in tags_lower or 'energy' in tags_lower:
category = cat_map['energy']
elif 'holy hydration' in tags_lower or 'hydration' in tags_lower:
category = cat_map['hydration']
@@ -59,8 +76,12 @@ class Command(BaseCommand):
elif 'milkshake' in tags_lower:
category = cat_map['milkshake']
# Skip if not a flavor product (e.g. merch, shakers)
if not category or 'merch' in tags_lower or 'shaker' in tags_lower:
# Skip only pure merch (clothing etc) if it's not a pack
if not category:
if 'merch' in tags_lower:
continue
# If we still don't have a category but it's a product, maybe put it in Packs/Other as fallback?
# For now, let's skip to avoid clutter unless we are sure.
continue
# Check availability
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-15 22:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0004_user_email_confirmation_code_user_pending_email'),
]
operations = [
migrations.AlterField(
model_name='user',
name='theme',
field=models.CharField(choices=[('latte', 'Latte'), ('frappe', 'Frappé'), ('macchiato', 'Macchiato'), ('mocha', 'Mocha'), ('pink', 'Pastel Pink')], default='mocha', max_length=20),
),
]
+28
View File
@@ -0,0 +1,28 @@
# Generated by Django 6.0.2 on 2026-02-15 22:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0005_alter_user_theme'),
]
operations = [
migrations.CreateModel(
name='Reply',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('rating', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='api.rating')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replies', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created_at'],
},
),
]
+13
View File
@@ -8,6 +8,7 @@ class User(AbstractUser):
('frappe', 'Frappé'),
('macchiato', 'Macchiato'),
('mocha', 'Mocha'),
('pink', 'Pastel Pink'),
]
theme = models.CharField(max_length=20, choices=THEME_CHOICES, default='mocha')
pending_email = models.EmailField(max_length=254, blank=True, null=True)
@@ -59,3 +60,15 @@ class Rating(models.Model):
def __str__(self):
return f"{self.user.username} - {self.flavor.name}: {self.score}"
class Reply(models.Model):
user = models.ForeignKey(User, related_name='replies', on_delete=models.CASCADE)
rating = models.ForeignKey(Rating, related_name='replies', on_delete=models.CASCADE)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f"Reply by {self.user.username} on {self.rating.id}"
+11 -2
View File
@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import User, Flavor, Category, Rating
from .models import User, Flavor, Category, Rating, Reply
class UserSerializer(serializers.ModelSerializer):
class Meta:
@@ -11,14 +11,23 @@ class CategorySerializer(serializers.ModelSerializer):
model = Category
fields = ['id', 'name', 'slug']
class ReplySerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True)
class Meta:
model = Reply
fields = ['id', 'user', 'rating', 'text', 'created_at']
read_only_fields = ['rating']
class RatingSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True)
flavor_name = serializers.CharField(source='flavor.name', read_only=True)
flavor_image = serializers.CharField(source='flavor.image_url', read_only=True)
replies = ReplySerializer(many=True, read_only=True)
class Meta:
model = Rating
fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at']
fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at', 'replies']
class FlavorSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
+67 -45
View File
@@ -4,11 +4,10 @@ from rest_framework.response import Response
from rest_framework.decorators import action
from django.db.models import Avg
from django_filters.rest_framework import DjangoFilterBackend
from .models import User, Flavor, Category, Rating
from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer
from .models import User, Flavor, Category, Rating, Reply
from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer, ReplySerializer
class FlavorViewSet(viewsets.ModelViewSet):
# ... (rest of FlavorViewSet)
queryset = Flavor.objects.annotate(average_rating=Avg('ratings__score')).order_by('-average_rating')
serializer_class = FlavorSerializer
filter_backends = [DjangoFilterBackend]
@@ -28,12 +27,35 @@ class RatingViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
# Ensure user can only have one rating per flavor
flavor = serializer.validated_data['flavor']
if Rating.objects.filter(user=self.request.user, flavor=flavor).exists():
raise serializers.ValidationError('You have already rated this flavor.')
serializer.save(user=self.request.user)
def get_queryset(self):
if self.action == 'list':
return Rating.objects.select_related('user', 'flavor')
return Rating.objects.filter(user=self.request.user)
if self.action in ['list', 'retrieve']:
return Rating.objects.select_related('user', 'flavor').prefetch_related('replies', 'replies__user')
# Allow users to see only their own ratings when editing/deleting?
# No, for detail views they need to find it.
# The permission class IsOwnerOrReadOnly (custom) would be better,
# but standard ViewSet logic handles "get_object" which we can protect.
return Rating.objects.select_related('user', 'flavor').prefetch_related('replies', 'replies__user')
def check_object_permissions(self, request, obj):
super().check_object_permissions(request, obj)
if request.method in ['PUT', 'PATCH', 'DELETE'] and obj.user != request.user:
self.permission_denied(request, message='You cannot edit/delete this rating.')
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])
def reply(self, request, pk=None):
rating = self.get_object()
text = request.data.get('text')
if not text:
return Response({'error': 'Text is required'}, status=status.HTTP_400_BAD_REQUEST)
reply = Reply.objects.create(user=request.user, rating=rating, text=text)
return Response(ReplySerializer(reply).data, status=status.HTTP_201_CREATED)
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
@@ -105,45 +127,6 @@ class UserViewSet(viewsets.ModelViewSet):
else:
return Response({'error': 'Invalid verification code'}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def request_password_reset(self, request):
email = request.data.get('email')
try:
user = User.objects.get(email=email, is_active=True)
except User.DoesNotExist:
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
import secrets
import string
code = ''.join(secrets.choice(string.digits) for _ in range(6))
user.email_confirmation_code = code
user.save()
send_mail(
'Password Reset Request',
f'Hi {user.username},\n\nYour password reset code is: {code}',
'noreply@holyflavors.com',
[email],
fail_silently=False,
)
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def complete_password_reset(self, request):
email = request.data.get('email')
code = request.data.get('code')
new_password = request.data.get('password')
try:
user = User.objects.get(email=email, email_confirmation_code=code)
except User.DoesNotExist:
return Response({'error': 'Invalid email or code'}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(new_password)
user.email_confirmation_code = None
user.save()
return Response({'status': 'Password reset successful!'})
@action(detail=False, methods=['get'])
def me(self, request):
serializer = self.get_serializer(request.user)
@@ -217,6 +200,45 @@ class UserViewSet(viewsets.ModelViewSet):
user.save()
return Response({'status': 'Email confirmed', 'email': user.email})
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def request_password_reset(self, request):
email = request.data.get('email')
try:
user = User.objects.get(email=email, is_active=True)
except User.DoesNotExist:
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
import secrets
import string
code = ''.join(secrets.choice(string.digits) for _ in range(6))
user.email_confirmation_code = code
user.save()
send_mail(
'Password Reset Request',
f'Hi {user.username},\n\nYour password reset code is: {code}',
'noreply@holyflavors.com',
[email],
fail_silently=False,
)
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def complete_password_reset(self, request):
email = request.data.get('email')
code = request.data.get('code')
new_password = request.data.get('password')
try:
user = User.objects.get(email=email, email_confirmation_code=code)
except User.DoesNotExist:
return Response({'error': 'Invalid email or code'}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(new_password)
user.email_confirmation_code = None
user.save()
return Response({'status': 'Password reset successful!'})
@action(detail=False, methods=['get'])
def dashboard(self, request):
user = request.user