Skip to content

Commit 8ec274c

Browse files
committed
Day 8: Product detail view, List view and Product rating and Review code
1 parent c7d2c61 commit 8ec274c

File tree

5 files changed

+225
-10
lines changed

5 files changed

+225
-10
lines changed

api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
AdminRegisterView,
66
AdminUserDetailView,
77
LoginView,
8+
ProductDetailView,
9+
ProductListView,
10+
ProductReviewCreateUpdateView,
811
TechnicianRegisterView,
912
UserRegisterView,
1013
)
@@ -18,6 +21,9 @@
1821
),
1922
path("register/admin/", AdminRegisterView.as_view(), name="admin-register"),
2023
path("login/", LoginView.as_view(), name="login"),
24+
path("products/", ProductListView.as_view()),
25+
path("products/<int:product_id>/", ProductDetailView.as_view()),
26+
path("products/<int:product_id>/review/", ProductReviewCreateUpdateView.as_view()),
2127
path("admin/dashboard/", AdminDashboardView.as_view(), name="admin-dashboard"),
2228
path(
2329
"admin/users/<int:user_id>/",

users/models.py

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
from django.conf import settings
22
from django.contrib.auth.models import AbstractUser
3+
from django.core.validators import MaxValueValidator, MinValueValidator
34
from django.db import models
45

56

7+
class TimestampedModel(models.Model):
8+
created_at = models.DateTimeField(auto_now_add=True)
9+
updated_at = models.DateTimeField(auto_now=True)
10+
11+
class Meta:
12+
abstract = True
13+
14+
615
class User(AbstractUser):
716
ROLE_CHOICES = [
817
("user", "User"),
@@ -22,7 +31,7 @@ def __str__(self):
2231
return str(self.name)
2332

2433

25-
class Profile(models.Model):
34+
class Profile(TimestampedModel):
2635
PROFILE_STATUS = [
2736
("inactive", "Inactive"),
2837
("pending", "Pending"),
@@ -51,14 +60,92 @@ def __str__(self):
5160
return str(self.user)
5261

5362

54-
class Review(models.Model):
63+
class Category(TimestampedModel):
64+
name = models.CharField(max_length=100, unique=True)
65+
66+
def __str__(self):
67+
return self.name
68+
69+
70+
class Brand(TimestampedModel):
71+
name = models.CharField(max_length=100, unique=True)
72+
category = models.ForeignKey(
73+
Category,
74+
on_delete=models.SET_NULL,
75+
null=True,
76+
blank=True,
77+
related_name="brands",
78+
)
79+
80+
def __str__(self):
81+
return str(self.name)
82+
83+
84+
class Product(TimestampedModel):
85+
product_name = models.CharField(max_length=255)
86+
description = models.TextField(blank=True, default="")
87+
price = models.DecimalField(max_digits=10, decimal_places=2)
88+
tags = models.ManyToManyField(Tag, blank=True)
89+
status = models.CharField(
90+
choices=[("stock", "Stock"), ("out_of_stock", "Out of Stock")], max_length=20
91+
)
92+
technical_specifications = models.JSONField(blank=True, null=True)
93+
brand = models.ForeignKey(
94+
Brand,
95+
on_delete=models.CASCADE,
96+
null=True,
97+
blank=True,
98+
related_name="products",
99+
)
100+
101+
def __str__(self):
102+
return str(self.product_name)
103+
104+
@property
105+
def average_rating(self):
106+
reviews = self.reviews.all()
107+
if reviews.exists():
108+
return round(sum([r.rating for r in reviews]) / reviews.count(), 2)
109+
return 0
110+
111+
112+
class ProductImage(models.Model):
113+
product = models.ForeignKey(
114+
Product, on_delete=models.CASCADE, related_name="images"
115+
)
116+
image = models.ImageField(upload_to="products/gallery/")
117+
118+
def __str__(self):
119+
return f"Image for {self.product.product_name}"
120+
121+
122+
class ProductReview(TimestampedModel):
123+
product = models.ForeignKey(
124+
Product, on_delete=models.CASCADE, related_name="reviews"
125+
)
126+
user = models.ForeignKey(
127+
User, on_delete=models.CASCADE, related_name="user_reviews"
128+
)
129+
rating = models.PositiveIntegerField(
130+
validators=[MinValueValidator(1), MaxValueValidator(5)]
131+
)
132+
review_text = models.TextField(blank=True)
133+
image = models.ImageField(upload_to="reviews/", blank=True, null=True)
134+
135+
class Meta:
136+
unique_together = ("user", "product") # One review per user
137+
138+
def __str__(self):
139+
return f"{self.user.username} - {self.product.product_name}"
140+
141+
142+
class Review(TimestampedModel):
55143
technician = models.ForeignKey(
56144
Profile, on_delete=models.CASCADE, related_name="reviews"
57145
)
58146
reviewer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
59147
rating = models.PositiveSmallIntegerField()
60148
review_text = models.TextField()
61-
created_at = models.DateTimeField(auto_now_add=True)
62149

63150
def __str__(self):
64151
return str(self.technician) + " - " + str(self.rating)

users/permissions.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,16 @@ def has_permission(self, request, view):
66
return bool(
77
request.user
88
and request.user.is_authenticated
9-
and getattr(request.user, "is_admin", False)
9+
and request.user.role == "admin"
10+
and request.user.is_superuser
11+
)
12+
13+
14+
class TechnicianUser(BasePermission):
15+
def has_permission(self, request, view):
16+
return bool(
17+
request.user
18+
and request.user.is_authenticated
19+
and request.user.role == "technician"
20+
and request.user.is_staff
1021
)

users/serializers.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
from drf_spectacular.utils import extend_schema_field
55
from rest_framework import serializers
66

7-
from users.models import Booking, Profile
7+
from users.models import (
8+
Booking,
9+
Brand,
10+
Product,
11+
ProductImage,
12+
ProductReview,
13+
Profile,
14+
Tag,
15+
)
816

917
User = get_user_model()
1018

@@ -62,10 +70,11 @@ def validate(self, attrs):
6270
def create(self, validated_data):
6371
validated_data.pop("confirm_password")
6472
password = validated_data.pop("password")
65-
if validated_data.get("role") == "admin":
66-
validated_data["is_admin"] = True
67-
# validated_data["is_superuser"] = True
6873
user = User(**validated_data)
74+
if validated_data.get("role") == "admin":
75+
user.is_superuser = True
76+
if validated_data.get("role") == "technician":
77+
user.is_staff = True
6978
user.set_password(password)
7079
user.save()
7180
return user
@@ -94,6 +103,64 @@ def update(self, instance, validated_data):
94103
return instance # Login does not update objects
95104

96105

106+
class ProductImageSerializer(serializers.ModelSerializer):
107+
class Meta:
108+
model = ProductImage
109+
fields = ["id", "image"]
110+
111+
112+
class BrandSerializer(serializers.ModelSerializer):
113+
category = serializers.StringRelatedField()
114+
115+
class Meta:
116+
model = Brand
117+
fields = ["id", "name", "category"]
118+
119+
120+
class TagSerializer(serializers.ModelSerializer):
121+
class Meta:
122+
model = Tag
123+
fields = ["id", "name"]
124+
125+
126+
class ProductReviewSerializer(serializers.ModelSerializer):
127+
username = serializers.CharField(source="user.username", read_only=True)
128+
129+
class Meta:
130+
model = ProductReview
131+
fields = ["id", "username", "rating", "review_text", "image", "created_at"]
132+
read_only_fields = ["username", "created_at"]
133+
134+
135+
class ProductSerializer(serializers.ModelSerializer):
136+
brand = BrandSerializer()
137+
tags = TagSerializer(many=True)
138+
images = ProductImageSerializer(many=True)
139+
reviews = ProductReviewSerializer(many=True, read_only=True)
140+
average_rating = serializers.FloatField(read_only=True)
141+
url = serializers.SerializerMethodField()
142+
143+
class Meta:
144+
model = Product
145+
fields = [
146+
"id",
147+
"product_name",
148+
"description",
149+
"price",
150+
"tags",
151+
"status",
152+
"technical_specifications",
153+
"brand",
154+
"images",
155+
"reviews",
156+
"average_rating",
157+
"url",
158+
]
159+
160+
def get_url(self, obj):
161+
return f"/products/{obj.id}/"
162+
163+
97164
class TechnicianSummarySerializer(serializers.ModelSerializer):
98165
username = serializers.CharField(source="user.username")
99166
fullname = serializers.CharField(source="user.fullname")

users/views.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
from django.contrib.auth import get_user_model
44
from django.db.models import Avg, Count, Max, Sum
55
from rest_framework import status
6-
from rest_framework.permissions import AllowAny
6+
from rest_framework.permissions import AllowAny, IsAuthenticated
77
from rest_framework.response import Response
88
from rest_framework.views import APIView
99
from rest_framework_simplejwt.tokens import RefreshToken
1010

11-
from users.models import Booking, Profile
11+
from users.models import Booking, Product, ProductReview, Profile
1212
from users.permissions import AdminUser
1313
from users.serializers import (
1414
AdminUserDetailSerializer,
1515
BookingDetailSerializer,
1616
LoginSerializer,
17+
ProductReviewSerializer,
18+
ProductSerializer,
1719
RegisterSerializer,
1820
TechnicianSummarySerializer,
1921
UserBookingHistorySerializer,
@@ -171,6 +173,48 @@ def post(self, request):
171173
)
172174

173175

176+
class ProductListView(APIView):
177+
permission_classes = [IsAuthenticated]
178+
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)
183+
184+
185+
class ProductReviewCreateUpdateView(APIView):
186+
permission_classes = [IsAuthenticated]
187+
188+
def post(self, request, product_id):
189+
product = Product.objects.get(id=product_id)
190+
191+
try:
192+
review = ProductReview.objects.get(product=product, user=request.user)
193+
serializer = ProductReviewSerializer(
194+
review, data=request.data, partial=True
195+
)
196+
except ProductReview.DoesNotExist:
197+
serializer = ProductReviewSerializer(data=request.data)
198+
199+
if serializer.is_valid():
200+
serializer.save(product=product, user=request.user)
201+
return Response(
202+
{"message": "Review submitted successfully", "data": serializer.data},
203+
status=status.HTTP_200_OK,
204+
)
205+
206+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
207+
208+
209+
class ProductDetailView(APIView):
210+
permission_classes = [IsAuthenticated]
211+
212+
def get(self, request, product_id):
213+
product = Product.objects.get(id=product_id)
214+
serializer = ProductSerializer(product)
215+
return Response(serializer.data, status=status.HTTP_200_OK)
216+
217+
174218
# Admin Views all user information including roles and technician details.
175219

176220

0 commit comments

Comments
 (0)