Skip to content

Commit c83c201

Browse files
authored
Merge pull request #2 from PTHARRISH/Feature_Cart_and_Product
Feature cart and product
2 parents bad165e + 8b806ed commit c83c201

File tree

5 files changed

+609
-21
lines changed

5 files changed

+609
-21
lines changed

api/urls.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
from django.urls import path
22

33
from users.views import (
4+
AddToCartView,
45
AdminDashboardView,
56
AdminRegisterView,
67
AdminUserDetailView,
8+
CartDetailView,
9+
CheckoutView,
710
DeleteAccountView,
811
LoginView,
912
ProductDetailView,
1013
ProductListView,
1114
ProductReviewCreateUpdateView,
1215
ProfileView,
16+
RemoveCartItemView,
17+
TechnicianBookingsView,
18+
TechnicianNotificationsView,
1319
TechnicianRegisterView,
20+
TechnicianReviewView,
21+
UpdateCartItemView,
1422
UserRegisterView,
1523
)
1624

@@ -33,11 +41,35 @@
3341
ProductReviewCreateUpdateView.as_view(),
3442
name="product-review",
3543
),
44+
path("technician/review/<int:booking_id>/", TechnicianReviewView.as_view()),
3645
path("admin/dashboard/", AdminDashboardView.as_view(), name="admin-dashboard"),
3746
path(
3847
"admin/users/<int:user_id>/",
3948
AdminUserDetailView.as_view(),
4049
name="admin-user-detail",
4150
),
51+
path(
52+
"technician/bookings/",
53+
TechnicianBookingsView.as_view(),
54+
name="technician-bookings",
55+
),
56+
path(
57+
"technician/notifications/",
58+
TechnicianNotificationsView.as_view(),
59+
name="technician-notifications",
60+
),
61+
path("cart/add/", AddToCartView.as_view(), name="cart-add"),
62+
path("cart/", CartDetailView.as_view(), name="cart-detail"),
63+
path(
64+
"cart/item/<int:item_id>/",
65+
UpdateCartItemView.as_view(),
66+
name="cart-item-update",
67+
),
68+
path(
69+
"cart/item/<int:item_id>/remove/",
70+
RemoveCartItemView.as_view(),
71+
name="cart-item-remove",
72+
),
73+
path("cart/checkout/", CheckoutView.as_view(), name="cart-checkout"),
4274
path("delete-account/", DeleteAccountView.as_view(), name="delete-account"),
4375
]

users/models.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
from decimal import ROUND_HALF_UP, Decimal
2+
13
from django.conf import settings
24
from django.contrib.auth.models import AbstractUser
35
from django.core.validators import MaxValueValidator, MinValueValidator
46
from django.db import models
57

8+
PAYMENT_CHOICES = [
9+
("COD", "Cash on Delivery"),
10+
("UPI", "UPI"),
11+
("CARD", "Card"),
12+
]
13+
614

715
class TimestampedModel(models.Model):
816
created_at = models.DateTimeField(auto_now_add=True)
@@ -130,7 +138,7 @@ class ProductReview(TimestampedModel):
130138
validators=[MinValueValidator(1), MaxValueValidator(5)]
131139
)
132140
review_text = models.TextField(blank=True)
133-
image = models.ImageField(upload_to="reviews/", blank=True, null=True)
141+
# image = models.ImageField(upload_to="reviews/", blank=True, null=True)
134142

135143
class Meta:
136144
unique_together = ("user", "product") # One review per user
@@ -139,6 +147,17 @@ def __str__(self):
139147
return f"{self.user.username} - {self.product.product_name}"
140148

141149

150+
class ProductReviewImage(models.Model):
151+
review = models.ForeignKey(
152+
ProductReview, related_name="images", on_delete=models.CASCADE
153+
)
154+
image = models.ImageField(upload_to="product_review_images/")
155+
uploaded_at = models.DateTimeField(auto_now_add=True)
156+
157+
def __str__(self):
158+
return f"ReviewImage {self.id} for Review {self.review.id}"
159+
160+
142161
class Review(TimestampedModel):
143162
technician = models.ForeignKey(
144163
Profile, on_delete=models.CASCADE, related_name="reviews"
@@ -174,3 +193,103 @@ class Booking(models.Model):
174193

175194
def __str__(self):
176195
return f"Booking {self.pk} - {self.technician} for {self.user}"
196+
197+
198+
class Cart(TimestampedModel):
199+
user = models.ForeignKey(
200+
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="carts"
201+
)
202+
is_active = models.BooleanField(default=True)
203+
204+
def __str__(self):
205+
return f"Cart {self.pk} - {self.user.username}"
206+
207+
@property
208+
def total(self):
209+
total = sum([item.total_price for item in self.items.all()])
210+
return Decimal(total).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
211+
212+
213+
class CartItem(models.Model):
214+
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name="items")
215+
product = models.ForeignKey("Product", on_delete=models.CASCADE)
216+
quantity = models.PositiveIntegerField(default=1)
217+
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
218+
added_at = models.DateTimeField(auto_now_add=True)
219+
220+
class Meta:
221+
unique_together = ("cart", "product")
222+
223+
def __str__(self):
224+
return f"{self.product.product_name} x {self.quantity}"
225+
226+
@property
227+
def total_price(self):
228+
return (Decimal(self.unit_price) * Decimal(self.quantity)).quantize(
229+
Decimal("0.01"), rounding=ROUND_HALF_UP
230+
)
231+
232+
233+
class Order(models.Model):
234+
user = models.ForeignKey(
235+
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="orders"
236+
)
237+
cart = models.ForeignKey(Cart, on_delete=models.SET_NULL, null=True, blank=True)
238+
created_at = models.DateTimeField(auto_now_add=True)
239+
total = models.DecimalField(max_digits=12, decimal_places=2)
240+
address = models.JSONField(blank=True, null=True)
241+
payment_method = models.CharField(max_length=10, choices=PAYMENT_CHOICES)
242+
payment_done = models.BooleanField(default=False)
243+
technician = models.ForeignKey(
244+
"Profile", on_delete=models.SET_NULL, null=True, blank=True
245+
)
246+
technician_fee = models.DecimalField(max_digits=10, decimal_places=2, default=0)
247+
booking = models.OneToOneField(
248+
"Booking",
249+
on_delete=models.SET_NULL,
250+
null=True,
251+
blank=True,
252+
related_name="order",
253+
)
254+
notes = models.TextField(blank=True, default="")
255+
256+
def __str__(self):
257+
return f"Order {self.pk} - {self.user.username}"
258+
259+
260+
class OrderItem(models.Model):
261+
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
262+
product = models.ForeignKey("Product", on_delete=models.SET_NULL, null=True)
263+
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
264+
quantity = models.PositiveIntegerField()
265+
line_total = models.DecimalField(max_digits=12, decimal_places=2)
266+
267+
def __str__(self):
268+
return f"OrderItem {self.pk} for Order {self.order.pk}"
269+
270+
271+
class Notification(TimestampedModel):
272+
recipient = models.ForeignKey(
273+
"Profile", on_delete=models.CASCADE, related_name="notifications"
274+
)
275+
title = models.CharField(max_length=255)
276+
message = models.TextField()
277+
metadata = models.JSONField(blank=True, null=True)
278+
is_read = models.BooleanField(default=False)
279+
280+
def __str__(self):
281+
return f"Notification to {self.recipient} - {self.title}"
282+
283+
284+
class TechnicianReview(models.Model):
285+
technician = models.ForeignKey(
286+
Profile, on_delete=models.CASCADE, related_name="tech_reviews"
287+
)
288+
user = models.ForeignKey(User, on_delete=models.CASCADE)
289+
booking = models.OneToOneField(Booking, on_delete=models.CASCADE)
290+
rating = models.IntegerField()
291+
comment = models.TextField(blank=True)
292+
created_at = models.DateTimeField(auto_now_add=True)
293+
294+
def __str__(self):
295+
return f"{self.user} reviewed {self.technician}"

users/permissions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ def has_permission(self, request, view):
1919
and request.user.role == "technician"
2020
and request.user.is_staff
2121
)
22+
23+
24+
class IsUserRole(BasePermission):
25+
def has_permission(self, request, view):
26+
return bool(
27+
request.user
28+
and request.user.is_authenticated
29+
and request.user.role == "user"
30+
and not request.user.is_staff
31+
)

users/serializers.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
from rest_framework import serializers
55

66
from users.models import (
7+
PAYMENT_CHOICES,
78
Booking,
89
Brand,
10+
Cart,
11+
CartItem,
912
Product,
1013
ProductImage,
1114
ProductReview,
15+
ProductReviewImage,
1216
Profile,
1317
Tag,
18+
TechnicianReview,
1419
)
1520

1621
User = get_user_model()
@@ -184,12 +189,19 @@ class Meta:
184189
fields = ["id", "name"]
185190

186191

192+
class ProductReviewImageSerializer(serializers.ModelSerializer):
193+
class Meta:
194+
model = ProductReviewImage
195+
fields = ["id", "image", "uploaded_at"]
196+
197+
187198
class ProductReviewSerializer(serializers.ModelSerializer):
199+
images = ProductReviewImageSerializer(many=True, read_only=True)
188200
username = serializers.CharField(source="user.username", read_only=True)
189201

190202
class Meta:
191203
model = ProductReview
192-
fields = ["id", "username", "rating", "review_text", "image", "created_at"]
204+
fields = ["id", "username", "rating", "review_text", "images", "created_at"]
193205
read_only_fields = ["username", "created_at"]
194206

195207

@@ -326,3 +338,84 @@ def get_technician(self, obj):
326338
"username": t.username,
327339
"fullname": t.fullname,
328340
}
341+
342+
343+
class CartItemSerializer(serializers.ModelSerializer):
344+
product_name = serializers.CharField(source="product.product_name", read_only=True)
345+
total_price = serializers.DecimalField(
346+
source="total_price", max_digits=12, decimal_places=2, read_only=True
347+
)
348+
349+
class Meta:
350+
model = CartItem
351+
fields = [
352+
"id",
353+
"product",
354+
"product_name",
355+
"quantity",
356+
"unit_price",
357+
"total_price",
358+
]
359+
360+
361+
class CartSerializer(serializers.ModelSerializer):
362+
items = CartItemSerializer(many=True)
363+
total = serializers.DecimalField(
364+
source="total", max_digits=12, decimal_places=2, read_only=True
365+
)
366+
367+
class Meta:
368+
model = Cart
369+
fields = ["id", "user", "items", "total", "is_active"]
370+
read_only_fields = ["user", "total", "is_active"]
371+
372+
373+
class AddToCartSerializer(serializers.Serializer):
374+
product_id = serializers.IntegerField()
375+
quantity = serializers.IntegerField(min_value=1, default=1)
376+
377+
def validate_product_id(self, value):
378+
try:
379+
Product.objects.get(pk=value)
380+
except Product.DoesNotExist:
381+
raise serializers.ValidationError("Product not found.") from None
382+
return value
383+
384+
385+
class UpdateCartItemSerializer(serializers.Serializer):
386+
quantity = serializers.IntegerField(min_value=1)
387+
388+
389+
class CheckoutSerializer(serializers.Serializer):
390+
address_index = serializers.IntegerField(required=False)
391+
address = serializers.JSONField(required=False)
392+
payment_method = serializers.ChoiceField(choices=PAYMENT_CHOICES)
393+
technician_id = serializers.IntegerField(required=False, allow_null=True)
394+
date_time_start = serializers.DateTimeField(required=False, allow_null=True)
395+
date_time_end = serializers.DateTimeField(required=False, allow_null=True)
396+
payment_done = serializers.BooleanField(default=False)
397+
notes = serializers.CharField(required=False, allow_blank=True)
398+
399+
def validate(self, attrs):
400+
tech = attrs.get("technician_id")
401+
start = attrs.get("date_time_start")
402+
end = attrs.get("date_time_end")
403+
if tech:
404+
if not start or not end:
405+
raise serializers.ValidationError(
406+
"date_time_start and date_time_end "
407+
"are required when technician is selected."
408+
)
409+
if start >= end:
410+
raise serializers.ValidationError(
411+
"date_time_end must be after date_time_start."
412+
)
413+
if "address_index" not in attrs and "address" not in attrs:
414+
raise serializers.ValidationError("Provide address_index or address JSON.")
415+
return attrs
416+
417+
418+
class TechnicianReviewSerializer(serializers.ModelSerializer):
419+
class Meta:
420+
model = TechnicianReview
421+
fields = ["id", "rating", "comment", "created_at"]

0 commit comments

Comments
 (0)