diff --git a/api/models/categories.py b/api/models/categories.py index c7e1d3e..051bb0b 100644 --- a/api/models/categories.py +++ b/api/models/categories.py @@ -24,4 +24,9 @@ def save(self,*args,**kwargs): self.code = generate_category_code() super().save(*args,**kwargs) + @classmethod + def get_root_categories(cls): + """Custom method to get root categories""" + return cls.objects.filter(parent__isnull=True) + \ No newline at end of file diff --git a/api/serializers.py b/api/serializers.py index 66b8d21..b0d89c8 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -44,16 +44,45 @@ def create(self, validated_data): return Customer.objects.create(**validated_data) class CategorySerializer(serializers.ModelSerializer): - code = serializers.CharField(read_only=True) + children = serializers.SerializerMethodField() + products_count = serializers.IntegerField(read_only=True) class Meta: model = Category - fields = ['id','code', 'title', 'parent'] + fields = ['id', 'code', 'title', 'parent', 'children', 'products_count'] + + def get_children(self, obj): + # Limit recursion depth + if self.context.get('depth', 0) >= 2: + return [] + + # Create a new context with increased depth + context = dict(self.context) + context['depth'] = context.get('depth', 0) + 1 + + # Get direct children + children = obj.get_children() + return CategorySerializer(children, many=True, context=context).data + class ProductSerializer(serializers.ModelSerializer): - category = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all()) + category = serializers.PrimaryKeyRelatedField( + queryset=Category.objects.all(), + write_only=True + ) + category_details = serializers.SerializerMethodField(read_only=True) + class Meta: model = Product - fields = ['id','name', 'description', 'price', 'category'] + fields = ['id', 'name', 'description', 'price', 'category','category_details'] + + def get_category_details(self, obj): + # Prevent recursive serialization + return { + 'id': obj.category.id, + 'code': obj.category.code, + 'title': obj.category.title + } + class OrderItemSerializer(serializers.ModelSerializer): price = serializers.ReadOnlyField(source='product.price') diff --git a/api/views.py b/api/views.py index ba400fc..8cd4b7d 100644 --- a/api/views.py +++ b/api/views.py @@ -5,7 +5,7 @@ from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated,AllowAny from rest_framework.response import Response -from django.db.models import Avg +from django.db.models import Avg, Count from django.db.utils import OperationalError from api.auth import Auth0Authentication @@ -23,22 +23,68 @@ class ProductViewSet(viewsets.ModelViewSet): serializer_class = ProductSerializer permission_classes = [AllowAny] + @action(detail=False, methods=['get']) + def by_category(self, request): + """ + Get products filtered by category, including products in subcategories + """ + category_id = request.query_params.get('category_id') + if not category_id: + return Response( + {"error": "Category ID is required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + category = Category.objects.get(id=category_id) + + subcategories = category.get_descendants(include_self=True) + + products = Product.objects.filter(category__in=subcategories) + + serializer = self.get_serializer(products, many=True) + return Response(serializer.data) + + except Category.DoesNotExist: + return Response( + {"error": "Category not found"}, + status=status.HTTP_404_NOT_FOUND + ) + @action(detail=False, methods=['get']) def average_price(self, request): + """ + Calculate average price for a category and its subcategories + """ category_id = request.query_params.get('category_id') if not category_id: return Response({"error": "Category ID is required"}, status=status.HTTP_400_BAD_REQUEST) try: category = Category.objects.get(id=category_id) + subcategories = category.get_descendants(include_self=True) + + avg_price = Product.objects.filter( + category__in=subcategories + ).aggregate(avg_price=Avg('price'))['avg_price'] + + return Response({ + 'category_id': category_id, + 'category_title': category.title, + 'average_price': avg_price or 0, + 'total_products': Product.objects.filter(category__in=subcategories).count() + }) + except Category.DoesNotExist: return Response({"error": "Category not found"}, status=status.HTTP_404_NOT_FOUND) - subcategories = category.get_descendants(include_self=True) - avg_price = Product.objects.filter(category__in=subcategories).aggregate(avg_price=Avg('price'))['avg_price'] - - return Response({'category_id': category_id, 'average_price': avg_price or 0}) - + except Exception as e: + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all().prefetch_related('items__product') @@ -79,6 +125,36 @@ class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer + def get_queryset(self): + + return Category.objects.annotate( + products_count=Count('product', distinct=True) + ) + + @action(detail=False, methods=['get']) + def root_categories(self, request): + """ + Retrieve all root-level categories + """ + root_categories = Category.get_root_categories() + serializer = self.get_serializer(root_categories, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def hierarchy(self, request, pk=None): + """ + Retrieve the full category hierarchy starting from a specific category + """ + try: + category = self.get_object() + context = {'depth': 0} + serializer = self.get_serializer(category, context=context) + return Response(serializer.data) + except Category.DoesNotExist: + return Response( + {"error": "Category not found"}, + status=status.HTTP_404_NOT_FOUND + ) class HealthCheckView(viewsets.ViewSet): """ @@ -95,7 +171,7 @@ def get(self, request): 'message': 'Application is running and database is accessible' }) -#convert this to class + def health_check(request): """ Health check endpoint to verify application and database connectivity