Files

685 lines
27 KiB
Python

from django.conf import settings
from rest_framework import viewsets, status, filters, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny, IsAuthenticatedOrReadOnly
from django.db.models import Avg
from django.shortcuts import get_object_or_404
from django.http import HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from django.middleware.csrf import get_token
from rest_framework.exceptions import PermissionDenied
from django.conf import settings
from django.http import FileResponse, HttpResponseForbidden
from django.views.decorators.http import require_GET
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
import os
from .models import User, Category, Product, Rating, Order, OrderItem, Favorite, PrivacyPolicy, FAQ, Contact, Slider, Coupon, Notification
from .serializers import (
UserSerializer, CategorySerializer, ProductSerializer, ProductDetailSerializer,
RatingSerializer, OrderSerializer, OrderItemSerializer, FavoriteSerializer, PrivacyPolicySerializer,
FAQSerializer, ContactSerializer, SliderSerializer, CouponSerializer, NotificationSerializer
)
from django.contrib.auth import authenticate
from rest_framework.authtoken.models import Token
from rest_framework.views import APIView
# Add RegisterView class for user registration
class RegisterView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
# Create auth token for the new user
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user_id': user.id,
'username': user.username,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'message': 'User registered successfully'
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['username', 'email', 'first_name', 'last_name']
permission_classes = [AllowAny]
def get_queryset(self):
# If user_id query param is provided, filter by that
user_id = self.request.query_params.get('user_id', None)
if user_id:
return User.objects.filter(id=user_id)
# If no specific user_id is requested, and user is staff, show all
if self.request.user.is_authenticated and self.request.user.is_staff:
return User.objects.all()
# For anonymous or non-staff users requesting the list endpoint, return empty queryset
if self.action == 'list':
return User.objects.none()
# For other actions like retrieve, let Django's permission system handle it
return User.objects.all()
@action(detail=False, methods=['get'])
def me(self, request):
"""Get current user's profile"""
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED)
serializer = self.get_serializer(request.user)
return Response(serializer.data)
@action(detail=False, methods=['post'])
def update_profile_picture(self, request):
"""Update the current user's profile picture"""
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED)
if 'profile_picture' not in request.FILES:
return Response({'error': 'No profile picture provided'}, status=status.HTTP_400_BAD_REQUEST)
try:
user = request.user
# Save the new profile picture
user.profile_picture = request.FILES['profile_picture']
user.save()
# Return the updated user data
serializer = self.get_serializer(user)
response_data = serializer.data
# Add the profile picture URL to the response
if user.profile_picture:
response_data['profile_picture'] = request.build_absolute_uri(user.profile_picture.url)
return Response(response_data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'])
def update_profile(self, request):
"""Update the current user's profile details"""
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED)
try:
user = request.user
# Update user details
user.username = request.data.get('username', user.username)
user.first_name = request.data.get('first_name', user.first_name)
user.last_name = request.data.get('last_name', user.last_name)
user.phone = request.data.get('phone', user.phone)
user.email = request.data.get('email', user.email)
user.gender = request.data.get('gender', user.gender)
user.address = request.data.get('address', user.address)
user.save()
# Return the updated user data
serializer = self.get_serializer(user)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'])
def login(self, request):
"""Login user and return token"""
username = request.data.get('username')
password = request.data.get('password')
if not username or not password:
return Response(
{'error': 'Please provide both username and password'},
status=status.HTTP_400_BAD_REQUEST
)
user = authenticate(username=username, password=password)
if not user:
return Response(
{'error': 'Invalid credentials'},
status=status.HTTP_401_UNAUTHORIZED
)
token, _ = Token.objects.get_or_create(user=user)
serializer = self.get_serializer(user)
return Response({
'token': token.key,
'user': serializer.data
})
@action(detail=False, methods=['get'])
def gender_options(self, request):
"""Get available gender options"""
options = [
{'value': 'Male', 'label': 'Male'},
{'value': 'Female', 'label': 'Female'},
{'value': 'Other', 'label': 'Other'}
]
return Response(options)
@action(detail=False, methods=['post'])
def update_password(self, request):
"""Update the current user's password"""
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED)
current_password = request.data.get('current_password')
new_password = request.data.get('new_password')
if not current_password or not new_password:
return Response({'error': 'Both current and new passwords are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
user = request.user
# Verify current password
if not user.check_password(current_password):
return Response({'error': 'Current password is incorrect'}, status=status.HTTP_400_BAD_REQUEST)
# Update password
user.set_password(new_password)
user.save()
return Response({'message': 'Password updated successfully'})
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['post'])
def delete_account(self, request):
"""Delete the current user's account"""
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED)
password = request.data.get('password')
if not password:
return Response({'error': 'Password is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
user = request.user
# Verify password
if not user.check_password(password):
return Response({'error': 'Password is incorrect'}, status=status.HTTP_400_BAD_REQUEST)
# Delete the user
user.delete()
return Response({'message': 'Account deleted successfully'})
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
filter_backends = [filters.SearchFilter]
search_fields = ['name']
permission_classes = [AllowAny]
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
permission_classes = [IsAdminUser]
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
@action(detail=True, methods=['get'])
def products(self, request, pk=None):
try:
# Validate that pk is a valid integer
try:
category_id = int(pk)
except (ValueError, TypeError):
return Response(
{"error": "Invalid category ID. Please provide a valid integer."},
status=status.HTTP_400_BAD_REQUEST
)
# Get the category
try:
category = Category.objects.get(id=category_id)
except Category.DoesNotExist:
return Response(
{"error": f"Category with ID {category_id} not found"},
status=status.HTTP_404_NOT_FOUND
)
# Get products with related category
products = Product.objects.filter(category=category).select_related('category')
# Serialize the products
serializer = ProductSerializer(products, many=True, context={'request': request})
return Response(serializer.data)
except Exception as e:
print(f"Error in products action: {str(e)}")
print(f"Error type: {type(e)}")
import traceback
print(traceback.format_exc())
return Response(
{"error": f"An error occurred while fetching products: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name', 'description', 'category__name']
ordering_fields = ['name', 'price', 'created_at']
permission_classes = [AllowAny]
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
permission_classes = [IsAdminUser]
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
def get_serializer_class(self):
if self.action == 'retrieve':
return ProductDetailSerializer
return ProductSerializer
@action(detail=True, methods=['post'])
def rate(self, request, pk=None):
# Still require authentication for rating
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED)
product = self.get_object()
rating_value = request.data.get('rating')
comment = request.data.get('comment', '')
if not rating_value:
return Response({'error': 'Rating is required'}, status=status.HTTP_400_BAD_REQUEST)
# Update or create rating
rating, created = Rating.objects.update_or_create(
product=product,
user=request.user,
defaults={'rating': rating_value, 'comment': comment}
)
serializer = RatingSerializer(rating)
return Response(serializer.data)
class RatingViewSet(viewsets.ModelViewSet):
queryset = Rating.objects.all()
serializer_class = RatingSerializer
permission_classes = [AllowAny]
def get_permissions(self):
if self.action in ['update', 'partial_update', 'destroy']:
permission_classes = [IsAuthenticated]
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
def perform_create(self, serializer):
# Still require authentication for creating ratings
if not self.request.user.is_authenticated:
raise PermissionDenied('Authentication required to create ratings')
serializer.save(user=self.request.user)
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = OrderSerializer
filter_backends = [filters.OrderingFilter]
ordering_fields = ['created_at', 'status']
permission_classes = [AllowAny]
def get_queryset(self):
user_id = self.request.query_params.get('user_id', None)
if user_id:
return Order.objects.filter(user_id=user_id)
if self.request.user.is_authenticated:
if self.request.user.is_staff:
return Order.objects.all()
return Order.objects.filter(user=self.request.user)
# For anonymous users, return empty queryset
return Order.objects.none()
def perform_create(self, serializer):
# For creating orders, require authentication
if not self.request.user.is_authenticated:
raise PermissionDenied('Authentication required to create orders')
serializer.save(user=self.request.user)
def update(self, request, *args, **kwargs):
"""Override update to handle status changes and stock management"""
instance = self.get_object()
new_status = request.data.get('status')
# Check if status is being updated
if new_status and new_status != instance.status:
# Validate status transition
valid_transitions = {
'Pending': ['Shipped', 'Cancelled'],
'Shipped': ['Delivered', 'Cancelled'],
'Delivered': [], # No transitions allowed from Delivered
'Cancelled': [] # No transitions allowed from Cancelled
}
if new_status not in valid_transitions.get(instance.status, []):
return Response({
'error': f'Cannot transition from {instance.status} to {new_status}'
}, status=status.HTTP_400_BAD_REQUEST)
# Check stock availability for Shipped status
if new_status == 'Shipped':
for item in instance.order_items.all():
if item.product.stock < item.quantity:
return Response({
'error': f'Not enough stock for {item.product.name}. Available: {item.product.stock}'
}, status=status.HTTP_400_BAD_REQUEST)
# Proceed with update
return super().update(request, *args, **kwargs)
@action(detail=True, methods=['post'])
def cancel(self, request, pk=None):
order = self.get_object()
# Check if order belongs to current user or user is admin
if self.request.user.is_authenticated and (order.user.id == request.user.id or request.user.is_staff):
# Check if order can be canceled
if order.status not in ['Pending', 'Shipped']:
return Response({'error': 'Cannot cancel order in current status'},
status=status.HTTP_400_BAD_REQUEST)
# Update order status
order.status = 'Cancelled'
order.save()
# Notification will be created automatically via signal
serializer = OrderSerializer(order)
return Response(serializer.data)
else:
return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN)
@action(detail=True, methods=['post'])
def ship(self, request, pk=None):
"""Mark an order as shipped"""
order = self.get_object()
# Check if user is staff
if not request.user.is_staff:
return Response({'error': 'Only staff can ship orders'},
status=status.HTTP_403_FORBIDDEN)
# Check if order can be shipped
if order.status != 'Pending':
return Response({'error': 'Can only ship pending orders'},
status=status.HTTP_400_BAD_REQUEST)
# Check stock availability
for item in order.order_items.all():
if item.product.stock < item.quantity:
return Response({
'error': f'Not enough stock for {item.product.name}. Available: {item.product.stock}'
}, status=status.HTTP_400_BAD_REQUEST)
# Update order status
order.status = 'Shipped'
order.save()
# Notification will be created automatically via signal
serializer = OrderSerializer(order)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def deliver(self, request, pk=None):
"""Mark an order as delivered"""
order = self.get_object()
# Check if user is staff
if not request.user.is_staff:
return Response({'error': 'Only staff can mark orders as delivered'},
status=status.HTTP_403_FORBIDDEN)
# Check if order can be delivered
if order.status != 'Shipped':
return Response({'error': 'Can only deliver shipped orders'},
status=status.HTTP_400_BAD_REQUEST)
# Update order status
order.status = 'Delivered'
order.save()
# Notification will be created automatically via signal
serializer = OrderSerializer(order)
return Response(serializer.data)
class OrderItemViewSet(viewsets.ModelViewSet):
queryset = OrderItem.objects.all()
serializer_class = OrderItemSerializer
permission_classes = [AllowAny]
def get_queryset(self):
user_id = self.request.query_params.get('user_id', None)
if user_id:
return OrderItem.objects.filter(order__user_id=user_id)
if self.request.user.is_authenticated:
if self.request.user.is_staff:
return OrderItem.objects.all()
return OrderItem.objects.filter(order__user=self.request.user)
# For anonymous users, return empty queryset
return OrderItem.objects.none()
def get_csrf_token(request):
"""
Return CSRF token for the frontend to use in subsequent requests
"""
csrf_token = get_token(request)
return JsonResponse({'csrfToken': csrf_token})
@require_GET
@authentication_classes([TokenAuthentication])
@permission_classes([IsAuthenticated])
def serve_protected_media(request, path):
"""
Serve media files with authentication check.
The path parameter should be the relative path within MEDIA_ROOT.
"""
# Construct the full path
file_path = os.path.join(settings.MEDIA_ROOT, path)
# Basic security check to prevent directory traversal
if not os.path.abspath(file_path).startswith(os.path.abspath(settings.MEDIA_ROOT)):
return HttpResponseForbidden('Invalid path')
# Check if file exists
if not os.path.exists(file_path):
return HttpResponseForbidden('File not found')
# Serve the file
return FileResponse(open(file_path, 'rb'))
class FavoriteViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
def list(self, request):
"""Get all favorites for the current user"""
favorites = Favorite.objects.filter(user=request.user)
serializer = FavoriteSerializer(favorites, many=True, context={'request': request})
return Response(serializer.data)
@action(detail=False, methods=['post'])
def toggle(self, request):
"""Toggle favorite status for a product"""
product_id = request.data.get('product_id')
if not product_id:
return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
product = Product.objects.get(id=product_id)
except Product.DoesNotExist:
return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND)
# Check if favorite exists
favorite = Favorite.objects.filter(user=request.user, product=product).first()
if favorite:
# Remove from favorites
favorite.delete()
return Response({'message': 'Product removed from favorites'})
else:
# Add to favorites
Favorite.objects.create(user=request.user, product=product)
return Response({'message': 'Product added to favorites'})
# Return updated favorites list
favorites = Favorite.objects.filter(user=request.user)
serializer = FavoriteSerializer(favorites, many=True, context={'request': request})
return Response(serializer.data)
@action(detail=False, methods=['get'])
def check(self, request):
"""Check if a product is favorited"""
product_id = request.query_params.get('product_id')
if not product_id:
return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
product = Product.objects.get(id=product_id)
except Product.DoesNotExist:
return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND)
is_favorited = Favorite.objects.filter(user=request.user, product=product).exists()
return Response({'is_favorited': is_favorited})
class PrivacyPolicyViewSet(viewsets.ModelViewSet):
queryset = PrivacyPolicy.objects.filter(is_active=True)
serializer_class = PrivacyPolicySerializer
permission_classes = [AllowAny]
def get_queryset(self):
return PrivacyPolicy.objects.filter(is_active=True).order_by('-last_updated')
class FAQViewSet(viewsets.ModelViewSet):
queryset = FAQ.objects.filter(is_active=True)
serializer_class = FAQSerializer
permission_classes = [AllowAny]
def get_queryset(self):
category = self.request.query_params.get('category', None)
if category and category != 'All':
return FAQ.objects.filter(category=category, is_active=True)
return FAQ.objects.filter(is_active=True)
@action(detail=False, methods=['get'])
def categories(self, request):
"""Get all available FAQ categories"""
categories = FAQ.CATEGORY_CHOICES
return Response([{'value': value, 'label': label} for value, label in categories])
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.filter(is_active=True)
serializer_class = ContactSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
return Contact.objects.filter(is_active=True).order_by('contact_type')
class SliderViewSet(viewsets.ModelViewSet):
queryset = Slider.objects.filter(is_active=True)
serializer_class = SliderSerializer
permission_classes = [AllowAny]
def get_queryset(self):
return Slider.objects.filter(is_active=True).order_by('-created_at')
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
class CouponViewSet(viewsets.ModelViewSet):
queryset = Coupon.objects.all()
serializer_class = CouponSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
queryset = Coupon.objects.filter(is_active=True)
code = self.request.query_params.get('code', None)
if code:
queryset = queryset.filter(code=code)
return queryset
class ValidateCouponView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
code = request.data.get('code')
cart_total = request.data.get('cart_total', 0)
try:
coupon = Coupon.objects.get(code=code, is_active=True)
if not coupon.is_valid():
return Response({
'valid': False,
'message': 'Coupon has expired or is not yet active'
}, status=400)
if cart_total < coupon.min_purchase:
return Response({
'valid': False,
'message': f'Minimum purchase amount of ${coupon.min_purchase} required'
}, status=400)
discount = coupon.calculate_discount(cart_total)
return Response({
'valid': True,
'discount': discount,
'coupon': CouponSerializer(coupon).data
})
except Coupon.DoesNotExist:
return Response({
'valid': False,
'message': 'Invalid coupon code'
}, status=400)
class NotificationViewSet(viewsets.ModelViewSet):
serializer_class = NotificationSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Notification.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
@action(detail=True, methods=['put'])
def read(self, request, pk=None):
notification = self.get_object()
notification.read = True
notification.save()
return Response(self.get_serializer(notification).data)
@action(detail=False, methods=['delete'])
def clear_all(self, request):
self.get_queryset().delete()
return Response(status=status.HTTP_204_NO_CONTENT)