Skip to content

Commit 6b299a8

Browse files
committed
Day 9: API throttle, Product filter search order, pagination, Profile views,Serializer code
1 parent 2d3ea89 commit 6b299a8

File tree

3 files changed

+199
-28
lines changed

3 files changed

+199
-28
lines changed

backend/settings.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"django.contrib.messages",
4646
"django.contrib.staticfiles",
4747
"rest_framework",
48+
"django_filters",
4849
"drf_spectacular",
4950
]
5051

@@ -101,6 +102,9 @@
101102
}
102103

103104

105+
BASE_URL = config("BASE_URL", "http://127.0.0.1:8000")
106+
107+
104108
# Password validation
105109
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
106110

@@ -157,6 +161,22 @@
157161
"DEFAULT_AUTHENTICATION_CLASSES": (
158162
"rest_framework_simplejwt.authentication.JWTAuthentication",
159163
),
164+
"DEFAULT_THROTTLE_CLASSES": [
165+
"rest_framework.throttling.AnonRateThrottle",
166+
"rest_framework.throttling.ScopedRateThrottle",
167+
],
168+
"DEFAULT_THROTTLE_RATES": {
169+
"anon": "100/day",
170+
"login": "5/min",
171+
"product_list": "20/min",
172+
},
173+
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
174+
"PAGE_SIZE": 25,
175+
"DEFAULT_FILTER_BACKENDS": [
176+
"django_filters.rest_framework.DjangoFilterBackend",
177+
"rest_framework.filters.SearchFilter",
178+
"rest_framework.filters.OrderingFilter",
179+
],
160180
}
161181

162182
AUTH_USER_MODEL = "users.User"

users/serializers.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import re
22

33
from django.contrib.auth import get_user_model
4-
from drf_spectacular.utils import extend_schema_field
54
from rest_framework import serializers
65

76
from users.models import (
@@ -103,6 +102,68 @@ def update(self, instance, validated_data):
103102
return instance # Login does not update objects
104103

105104

105+
class UserProfileSerializer(serializers.ModelSerializer):
106+
fullname = serializers.CharField(source="user.fullname", required=False)
107+
108+
class Meta:
109+
model = Profile
110+
fields = [
111+
"fullname",
112+
"bio",
113+
"avatar",
114+
"profile_status",
115+
]
116+
read_only_fields = ["profile_status"]
117+
118+
def update(self, instance, validated_data):
119+
user_data = validated_data.pop("user", {})
120+
if "fullname" in user_data:
121+
instance.user.fullname = user_data["fullname"]
122+
instance.user.save()
123+
124+
return super().update(instance, validated_data)
125+
126+
127+
class TechnicianProfileSerializer(serializers.ModelSerializer):
128+
fullname = serializers.CharField(source="user.fullname", required=False)
129+
tags = serializers.PrimaryKeyRelatedField(
130+
queryset=Tag.objects.all(), many=True, required=False
131+
)
132+
133+
class Meta:
134+
model = Profile
135+
fields = [
136+
"fullname",
137+
"bio",
138+
"avatar",
139+
"profile_status",
140+
"years_experience",
141+
"months_experience",
142+
"tags",
143+
"available_days",
144+
"price_hour",
145+
"price_day",
146+
]
147+
148+
def update(self, instance, validated_data):
149+
user_data = validated_data.pop("user", {})
150+
if "fullname" in user_data:
151+
instance.user.fullname = user_data["fullname"]
152+
instance.user.save()
153+
154+
tags = validated_data.pop("tags", None)
155+
profile = super().update(instance, validated_data)
156+
157+
if tags is not None:
158+
profile.tags.set(tags)
159+
160+
return profile
161+
162+
163+
class AdminProfileSerializer(UserProfileSerializer):
164+
pass
165+
166+
106167
class ProductImageSerializer(serializers.ModelSerializer):
107168
class Meta:
108169
model = ProductImage
@@ -187,7 +248,6 @@ class Meta:
187248
"earnings",
188249
]
189250

190-
@extend_schema_field(serializers.CharField())
191251
def get_avatar(self, obj):
192252
return obj.avatar.url if obj.avatar else None
193253

@@ -235,7 +295,6 @@ class Meta:
235295
"avg_rating",
236296
]
237297

238-
@extend_schema_field(serializers.CharField())
239298
def get_avatar(self, obj):
240299
profile = getattr(obj, "profile", None)
241300
return profile.avatar.url if profile and profile.avatar else None

users/views.py

Lines changed: 117 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
11
from decimal import Decimal, InvalidOperation
22

33
from django.contrib.auth import get_user_model
4+
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
45
from django.db.models import Avg, Count, Max, Sum
6+
from django.http import JsonResponse
7+
from django.shortcuts import get_object_or_404
8+
from django_filters.rest_framework import DjangoFilterBackend
59
from rest_framework import status
10+
from rest_framework.filters import OrderingFilter, SearchFilter
11+
from rest_framework.generics import ListAPIView
612
from rest_framework.permissions import AllowAny, IsAuthenticated
713
from rest_framework.response import Response
14+
from rest_framework.throttling import ScopedRateThrottle
815
from rest_framework.views import APIView
916
from rest_framework_simplejwt.tokens import RefreshToken
1017

1118
from users.models import Booking, Product, ProductReview, Profile
1219
from users.permissions import AdminUser
1320
from users.serializers import (
21+
AdminProfileSerializer,
1422
AdminUserDetailSerializer,
1523
BookingDetailSerializer,
1624
LoginSerializer,
1725
ProductReviewSerializer,
1826
ProductSerializer,
1927
RegisterSerializer,
28+
TechnicianProfileSerializer,
2029
TechnicianSummarySerializer,
2130
UserBookingHistorySerializer,
31+
UserProfileSerializer,
2232
)
2333
from users.utils import paginate, parse_date_range
2434

@@ -82,23 +92,15 @@ def post(self, request):
8292
class LoginView(APIView):
8393
permission_classes = [AllowAny]
8494
serializer_class = LoginSerializer
95+
throttle_classes = [ScopedRateThrottle]
96+
throttle_scope = "login"
8597

8698
def post(self, request):
8799
# Validate input using serializer
88100
serializer = self.serializer_class(data=request.data)
89101
if not serializer.is_valid():
90102
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
91103

92-
# Debug logging — visible in Django terminal
93-
print("=" * 60)
94-
print("[LOGIN REQUEST] Received POST request!")
95-
print(f"[IP] {request.META.get('REMOTE_ADDR', 'Unknown')}")
96-
print(f"[PATH] {request.path}")
97-
print(f"[METHOD] {request.method}")
98-
print(f"[CONTENT-TYPE] {request.content_type}")
99-
print(f"[DATA] {dict(request.data)}")
100-
print("=" * 60)
101-
102104
identifier = serializer.validated_data.get("identifier").strip()
103105
password = serializer.validated_data.get("password")
104106

@@ -155,7 +157,7 @@ def post(self, request):
155157
refresh = RefreshToken.for_user(user)
156158

157159
# Auto-redirect URL based on role
158-
redirect_url = f"/{user.role}/{user.username}/"
160+
redirect_url = f"/profile/{user.username}/"
159161

160162
return Response(
161163
{
@@ -173,29 +175,91 @@ def post(self, request):
173175
)
174176

175177

176-
class ProductListView(APIView):
178+
class ProfileView(APIView):
177179
permission_classes = [IsAuthenticated]
178180

179-
def get(self, request):
180-
products = Product.objects.all().prefetch_related("images", "tags", "reviews")
181-
serializer = ProductSerializer(products, many=True)
182-
return Response(serializer.data, status=status.HTTP_200_OK)
181+
def get_profile(self, username):
182+
try:
183+
user = User.objects.get(username=username)
184+
profile = Profile.objects.filter(user=user).first()
185+
if not profile:
186+
return None, None
187+
return user, profile
188+
except User.DoesNotExist:
189+
return None, None
190+
191+
def get_serializer_class(self, request_user, profile_user):
192+
if request_user.role == "admin":
193+
return AdminProfileSerializer
194+
195+
if request_user.role == "technician" and request_user == profile_user:
196+
return TechnicianProfileSerializer
197+
198+
if request_user.role == "user" and request_user == profile_user:
199+
return UserProfileSerializer
200+
201+
return None
202+
203+
def get(self, request, username):
204+
user, profile = self.get_profile(username)
205+
if not user:
206+
return Response({"error": "User not found"}, status=404)
207+
208+
serializer_class = self.get_serializer_class(request.user, user)
209+
if not serializer_class:
210+
return Response({"error": "Permission denied"}, status=403)
211+
212+
serializer = serializer_class(profile)
213+
return Response(serializer.data)
214+
215+
def put(self, request, username):
216+
user, profile = self.get_profile(username)
217+
if not user:
218+
return Response({"error": "User not found"}, status=404)
219+
220+
serializer_class = self.get_serializer_class(request.user, user)
221+
if not serializer_class:
222+
return Response({"error": "Permission denied"}, status=403)
223+
224+
serializer = serializer_class(profile, data=request.data, partial=True)
225+
226+
if serializer.is_valid():
227+
serializer.save()
228+
return Response({"message": "Profile updated successfully"})
229+
return Response(serializer.errors, status=400)
230+
231+
232+
class ProductListView(ListAPIView):
233+
permission_classes = [IsAuthenticated]
234+
throttle_classes = [ScopedRateThrottle]
235+
throttle_scope = "product_list"
236+
serializer_class = ProductSerializer
237+
queryset = Product.objects.all().prefetch_related("images", "tags", "reviews")
238+
239+
filter_backends = [
240+
DjangoFilterBackend,
241+
SearchFilter,
242+
OrderingFilter,
243+
]
244+
245+
filterset_fields = ["brand", "status", "price"]
246+
search_fields = ["product_name", "description"]
247+
ordering_fields = ["price", "product_name", "average_rating"]
248+
ordering = ["price"]
183249

184250

185251
class ProductReviewCreateUpdateView(APIView):
186252
permission_classes = [IsAuthenticated]
253+
serializer_class = ProductReviewSerializer
187254

188255
def post(self, request, product_id):
189-
product = Product.objects.get(id=product_id)
256+
product = get_object_or_404(Product, id=product_id)
190257

191258
try:
192259
review = ProductReview.objects.get(product=product, user=request.user)
193-
serializer = ProductReviewSerializer(
194-
review, data=request.data, partial=True
195-
)
260+
serializer = self.serializer_class(review, data=request.data, partial=True)
196261
except ProductReview.DoesNotExist:
197-
serializer = ProductReviewSerializer(data=request.data)
198-
262+
serializer = self.serializer_class(data=request.data)
199263
if serializer.is_valid():
200264
serializer.save(product=product, user=request.user)
201265
return Response(
@@ -208,10 +272,11 @@ def post(self, request, product_id):
208272

209273
class ProductDetailView(APIView):
210274
permission_classes = [IsAuthenticated]
275+
serializer_class = ProductSerializer
211276

212277
def get(self, request, product_id):
213-
product = Product.objects.get(id=product_id)
214-
serializer = ProductSerializer(product)
278+
product = get_object_or_404(Product, id=product_id)
279+
serializer = self.serializer_class(product)
215280
return Response(serializer.data, status=status.HTTP_200_OK)
216281

217282

@@ -237,7 +302,7 @@ def get(self, request):
237302
# ===== If technician detail is requested =====
238303
tech_id = request.GET.get("technician_id")
239304
if tech_id:
240-
tech = Profile.objects.get(id=tech_id)
305+
tech = get_object_or_404(Profile, id=tech_id)
241306
tech_bookings = Booking.objects.filter(technician=tech).order_by(
242307
"-date_time_start"
243308
)
@@ -308,6 +373,8 @@ def get(self, request, user_id):
308373
)
309374
.first()
310375
)
376+
if not user_stats:
377+
return Response({"error": "User not found"}, status=404)
311378

312379
summary_data = self.serializer_class(user_stats).data
313380

@@ -328,3 +395,28 @@ def get(self, request, user_id):
328395
},
329396
}
330397
)
398+
399+
400+
signer = TimestampSigner()
401+
402+
403+
class DeleteAccountView(APIView):
404+
permission_classes = [AllowAny]
405+
406+
def get(self, request):
407+
token = request.GET.get("token")
408+
if not token:
409+
return JsonResponse({"error": "Token required"}, status=400)
410+
411+
try:
412+
# Verify and check expiration (max_age in seconds = 24h)
413+
unsigned = signer.unsign(token, max_age=86400)
414+
user = get_user_model().objects.get(pk=unsigned)
415+
except SignatureExpired:
416+
return JsonResponse({"error": "Link expired"}, status=400)
417+
except (BadSignature, get_user_model().DoesNotExist):
418+
return JsonResponse({"error": "Invalid link"}, status=400)
419+
420+
# Delete user and profile
421+
user.delete()
422+
return JsonResponse({"message": "Account deleted successfully."}, status=200)

0 commit comments

Comments
 (0)