Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions api/urls.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
from django.urls import path

from users.views import (
AddToCartView,
AdminDashboardView,
AdminRegisterView,
AdminUserDetailView,
CartDetailView,
CheckoutView,
DeleteAccountView,
LoginView,
ProductDetailView,
ProductListView,
ProductReviewCreateUpdateView,
ProfileView,
RemoveCartItemView,
TechnicianBookingsView,
TechnicianNotificationsView,
TechnicianRegisterView,
TechnicianReviewView,
UpdateCartItemView,
UserRegisterView,
)

Expand All @@ -33,11 +41,35 @@
ProductReviewCreateUpdateView.as_view(),
name="product-review",
),
path("technician/review/<int:booking_id>/", TechnicianReviewView.as_view()),
path("admin/dashboard/", AdminDashboardView.as_view(), name="admin-dashboard"),
path(
"admin/users/<int:user_id>/",
AdminUserDetailView.as_view(),
name="admin-user-detail",
),
path(
"technician/bookings/",
TechnicianBookingsView.as_view(),
name="technician-bookings",
),
path(
"technician/notifications/",
TechnicianNotificationsView.as_view(),
name="technician-notifications",
),
path("cart/add/", AddToCartView.as_view(), name="cart-add"),
path("cart/", CartDetailView.as_view(), name="cart-detail"),
path(
"cart/item/<int:item_id>/",
UpdateCartItemView.as_view(),
name="cart-item-update",
),
path(
"cart/item/<int:item_id>/remove/",
RemoveCartItemView.as_view(),
name="cart-item-remove",
),
path("cart/checkout/", CheckoutView.as_view(), name="cart-checkout"),
path("delete-account/", DeleteAccountView.as_view(), name="delete-account"),
]
121 changes: 120 additions & 1 deletion users/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from decimal import ROUND_HALF_UP, Decimal

from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models

PAYMENT_CHOICES = [
("COD", "Cash on Delivery"),
("UPI", "UPI"),
("CARD", "Card"),
]


class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -130,7 +138,7 @@ class ProductReview(TimestampedModel):
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
review_text = models.TextField(blank=True)
image = models.ImageField(upload_to="reviews/", blank=True, null=True)
# image = models.ImageField(upload_to="reviews/", blank=True, null=True)

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


class ProductReviewImage(models.Model):
review = models.ForeignKey(
ProductReview, related_name="images", on_delete=models.CASCADE
)
image = models.ImageField(upload_to="product_review_images/")
uploaded_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"ReviewImage {self.id} for Review {self.review.id}"


class Review(TimestampedModel):
technician = models.ForeignKey(
Profile, on_delete=models.CASCADE, related_name="reviews"
Expand Down Expand Up @@ -174,3 +193,103 @@ class Booking(models.Model):

def __str__(self):
return f"Booking {self.pk} - {self.technician} for {self.user}"


class Cart(TimestampedModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="carts"
)
is_active = models.BooleanField(default=True)

def __str__(self):
return f"Cart {self.pk} - {self.user.username}"

@property
def total(self):
total = sum([item.total_price for item in self.items.all()])
return Decimal(total).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey("Product", on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
added_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ("cart", "product")

def __str__(self):
return f"{self.product.product_name} x {self.quantity}"

@property
def total_price(self):
return (Decimal(self.unit_price) * Decimal(self.quantity)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)


class Order(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="orders"
)
cart = models.ForeignKey(Cart, on_delete=models.SET_NULL, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
total = models.DecimalField(max_digits=12, decimal_places=2)
address = models.JSONField(blank=True, null=True)
payment_method = models.CharField(max_length=10, choices=PAYMENT_CHOICES)
payment_done = models.BooleanField(default=False)
technician = models.ForeignKey(
"Profile", on_delete=models.SET_NULL, null=True, blank=True
)
technician_fee = models.DecimalField(max_digits=10, decimal_places=2, default=0)
booking = models.OneToOneField(
"Booking",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="order",
)
notes = models.TextField(blank=True, default="")

def __str__(self):
return f"Order {self.pk} - {self.user.username}"


class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey("Product", on_delete=models.SET_NULL, null=True)
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField()
line_total = models.DecimalField(max_digits=12, decimal_places=2)

def __str__(self):
return f"OrderItem {self.pk} for Order {self.order.pk}"


class Notification(TimestampedModel):
recipient = models.ForeignKey(
"Profile", on_delete=models.CASCADE, related_name="notifications"
)
title = models.CharField(max_length=255)
message = models.TextField()
metadata = models.JSONField(blank=True, null=True)
is_read = models.BooleanField(default=False)

def __str__(self):
return f"Notification to {self.recipient} - {self.title}"


class TechnicianReview(models.Model):
technician = models.ForeignKey(
Profile, on_delete=models.CASCADE, related_name="tech_reviews"
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
booking = models.OneToOneField(Booking, on_delete=models.CASCADE)
rating = models.IntegerField()
comment = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"{self.user} reviewed {self.technician}"
10 changes: 10 additions & 0 deletions users/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,13 @@ def has_permission(self, request, view):
and request.user.role == "technician"
and request.user.is_staff
)


class IsUserRole(BasePermission):
def has_permission(self, request, view):
return bool(
request.user
and request.user.is_authenticated
and request.user.role == "user"
and not request.user.is_staff
)
95 changes: 94 additions & 1 deletion users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
from rest_framework import serializers

from users.models import (
PAYMENT_CHOICES,
Booking,
Brand,
Cart,
CartItem,
Product,
ProductImage,
ProductReview,
ProductReviewImage,
Profile,
Tag,
TechnicianReview,
)

User = get_user_model()
Expand Down Expand Up @@ -184,12 +189,19 @@ class Meta:
fields = ["id", "name"]


class ProductReviewImageSerializer(serializers.ModelSerializer):
class Meta:
model = ProductReviewImage
fields = ["id", "image", "uploaded_at"]


class ProductReviewSerializer(serializers.ModelSerializer):
images = ProductReviewImageSerializer(many=True, read_only=True)
username = serializers.CharField(source="user.username", read_only=True)

class Meta:
model = ProductReview
fields = ["id", "username", "rating", "review_text", "image", "created_at"]
fields = ["id", "username", "rating", "review_text", "images", "created_at"]
read_only_fields = ["username", "created_at"]


Expand Down Expand Up @@ -326,3 +338,84 @@ def get_technician(self, obj):
"username": t.username,
"fullname": t.fullname,
}


class CartItemSerializer(serializers.ModelSerializer):
product_name = serializers.CharField(source="product.product_name", read_only=True)
total_price = serializers.DecimalField(
source="total_price", max_digits=12, decimal_places=2, read_only=True
)

class Meta:
model = CartItem
fields = [
"id",
"product",
"product_name",
"quantity",
"unit_price",
"total_price",
]


class CartSerializer(serializers.ModelSerializer):
items = CartItemSerializer(many=True)
total = serializers.DecimalField(
source="total", max_digits=12, decimal_places=2, read_only=True
)

class Meta:
model = Cart
fields = ["id", "user", "items", "total", "is_active"]
read_only_fields = ["user", "total", "is_active"]


class AddToCartSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1, default=1)

def validate_product_id(self, value):
try:
Product.objects.get(pk=value)
except Product.DoesNotExist:
raise serializers.ValidationError("Product not found.") from None
return value


class UpdateCartItemSerializer(serializers.Serializer):
quantity = serializers.IntegerField(min_value=1)


class CheckoutSerializer(serializers.Serializer):
address_index = serializers.IntegerField(required=False)
address = serializers.JSONField(required=False)
payment_method = serializers.ChoiceField(choices=PAYMENT_CHOICES)
technician_id = serializers.IntegerField(required=False, allow_null=True)
date_time_start = serializers.DateTimeField(required=False, allow_null=True)
date_time_end = serializers.DateTimeField(required=False, allow_null=True)
payment_done = serializers.BooleanField(default=False)
notes = serializers.CharField(required=False, allow_blank=True)

def validate(self, attrs):
tech = attrs.get("technician_id")
start = attrs.get("date_time_start")
end = attrs.get("date_time_end")
if tech:
if not start or not end:
raise serializers.ValidationError(
"date_time_start and date_time_end "
"are required when technician is selected."
)
if start >= end:
raise serializers.ValidationError(
"date_time_end must be after date_time_start."
)
if "address_index" not in attrs and "address" not in attrs:
raise serializers.ValidationError("Provide address_index or address JSON.")
return attrs


class TechnicianReviewSerializer(serializers.ModelSerializer):
class Meta:
model = TechnicianReview
fields = ["id", "rating", "comment", "created_at"]
Loading