334 lines
12 KiB
Python
334 lines
12 KiB
Python
from django.db import models
|
|
from django.contrib.auth.hashers import make_password, check_password
|
|
from django.dispatch import receiver
|
|
from rest_framework.authtoken.models import Token
|
|
from django.db.models.signals import post_save
|
|
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
|
|
class UserManager(BaseUserManager):
|
|
def create_user(self, username, email, password=None, **extra_fields):
|
|
if not email:
|
|
raise ValueError('Users must have an email address')
|
|
if not username:
|
|
raise ValueError('Users must have a username')
|
|
|
|
email = self.normalize_email(email)
|
|
user = self.model(
|
|
username=username,
|
|
email=email,
|
|
**extra_fields
|
|
)
|
|
user.set_password(password)
|
|
user.save(using=self._db)
|
|
return user
|
|
|
|
def create_superuser(self, username, email, password=None, **extra_fields):
|
|
extra_fields.setdefault('is_staff', True)
|
|
extra_fields.setdefault('is_superuser', True)
|
|
|
|
return self.create_user(username, email, password, **extra_fields)
|
|
|
|
class User(AbstractBaseUser, PermissionsMixin):
|
|
id = models.BigAutoField(primary_key=True)
|
|
username = models.CharField(max_length=150, unique=True)
|
|
first_name = models.CharField(max_length=30)
|
|
last_name = models.CharField(max_length=30)
|
|
email = models.EmailField(unique=True)
|
|
is_active = models.BooleanField(default=True)
|
|
is_staff = models.BooleanField(default=False)
|
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
|
address = models.TextField(blank=True, null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
phone_number = models.CharField(max_length=15, blank=True, null=True)
|
|
|
|
objects = UserManager()
|
|
|
|
USERNAME_FIELD = 'username'
|
|
EMAIL_FIELD = 'email'
|
|
REQUIRED_FIELDS = ['email', 'first_name', 'last_name']
|
|
|
|
def __str__(self):
|
|
return self.username
|
|
|
|
def set_password(self, raw_password):
|
|
self.password = make_password(raw_password)
|
|
|
|
def check_password(self, raw_password):
|
|
return check_password(raw_password, self.password)
|
|
|
|
class Meta:
|
|
verbose_name = 'User'
|
|
verbose_name_plural = 'Users'
|
|
ordering = ['username']
|
|
|
|
def get_full_name(self):
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
def get_short_name(self):
|
|
return self.first_name
|
|
|
|
@receiver(post_save, sender=User)
|
|
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
|
if created:
|
|
Token.objects.create(user=instance)
|
|
|
|
class Category(models.Model):
|
|
id = models.BigAutoField(primary_key=True)
|
|
name = models.CharField(max_length=100, unique=True)
|
|
image = models.ImageField(upload_to='categories/', blank=True, null=True)
|
|
description = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
class Meta:
|
|
verbose_name = 'Category'
|
|
verbose_name_plural = 'Categories'
|
|
ordering = ['name']
|
|
@property
|
|
def image_url(self):
|
|
if self.image:
|
|
return f'/media/{self.image.name}'
|
|
return None
|
|
|
|
class Product(models.Model):
|
|
id = models.BigAutoField(primary_key= True)
|
|
name = models.CharField(max_length=100)
|
|
description = models.TextField()
|
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
image = models.ImageField(upload_to='products/', blank=True, null=True)
|
|
category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)
|
|
size = models.CharField(max_length=10, blank=True, null=True)
|
|
color = models.CharField(max_length=20, blank=True, null=True)
|
|
stock = models.IntegerField(default=0)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
class Meta:
|
|
verbose_name = 'Product'
|
|
verbose_name_plural = 'Products'
|
|
ordering = ['name']
|
|
@property
|
|
def image_url(self):
|
|
if self.image:
|
|
return f'/media/{self.image.name}'
|
|
return None
|
|
|
|
def update_average_rating(self):
|
|
avg_rating = self.ratings.aggregate(avg_rating=models.Avg('rating'))['avg_rating']
|
|
self.average_rating = avg_rating or 0
|
|
self.save()
|
|
|
|
class Rating(models.Model):
|
|
id = models.BigAutoField(primary_key= True)
|
|
product = models.ForeignKey(Product, related_name='ratings', on_delete=models.CASCADE)
|
|
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
|
|
rating = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
|
comment = models.TextField(blank=True, null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
class Meta:
|
|
unique_together = ('product', 'user')
|
|
verbose_name = 'Rating'
|
|
verbose_name_plural = 'Ratings'
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f'{self.user.username} - {self.product.name} - {self.rating}'
|
|
|
|
class Order(models.Model):
|
|
id = models.AutoField(primary_key=True)
|
|
user = models.ForeignKey(User, related_name='orders', on_delete=models.CASCADE)
|
|
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
|
status = models.CharField(max_length=20, default='Pending', choices=[
|
|
('Pending', 'Pending'),
|
|
('Shipped', 'Shipped'),
|
|
('Delivered', 'Delivered'),
|
|
('Cancelled', 'Cancelled')
|
|
])
|
|
shipping_address = models.CharField(max_length=255, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f'Order {self.id} by {self.user.username}'
|
|
|
|
def update_total(self):
|
|
"""Automatically calculate total price from order items"""
|
|
order_items = self.order_items.all()
|
|
if not order_items.exists():
|
|
self.total_price = 0
|
|
else:
|
|
self.total_price = sum(item.get_subtotal() for item in order_items)
|
|
self.save()
|
|
|
|
def add_product(self, product, quantity=1):
|
|
"""Add a product to the order"""
|
|
if product is None:
|
|
return None
|
|
|
|
if quantity is None or quantity <= 0:
|
|
quantity = 1
|
|
|
|
order_item, created = OrderItem.objects.get_or_create(
|
|
order=self,
|
|
product=product,
|
|
defaults={
|
|
'quantity': quantity,
|
|
'price': product.price if product.price else 0
|
|
}
|
|
)
|
|
|
|
if not created:
|
|
order_item.quantity += quantity
|
|
order_item.save()
|
|
|
|
self.update_total()
|
|
return order_item
|
|
|
|
class Meta:
|
|
verbose_name = 'Order'
|
|
verbose_name_plural = 'Orders'
|
|
ordering = ['-created_at']
|
|
|
|
class OrderItem(models.Model):
|
|
order = models.ForeignKey(Order, related_name='order_items', on_delete=models.CASCADE)
|
|
product = models.ForeignKey(Product, related_name='order_items', on_delete=models.CASCADE)
|
|
quantity = models.IntegerField()
|
|
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
|
|
|
def get_subtotal(self):
|
|
"""Calculate the subtotal for this item"""
|
|
if self.price is None or self.quantity is None:
|
|
return 0
|
|
return self.price * self.quantity
|
|
|
|
def __str__(self):
|
|
return f'{self.product.name} - {self.quantity}'
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Ensure price is set correctly if it's none or a new order item
|
|
if self.price is None or (not self.pk and not self.price):
|
|
self.price = self.product.price if self.product and self.product.price else 0
|
|
|
|
# Ensure quantity is never None
|
|
if self.quantity is None:
|
|
self.quantity = 1
|
|
|
|
super().save(*args, **kwargs)
|
|
# Update the order total when an item is saved
|
|
if self.order:
|
|
self.order.update_total()
|
|
|
|
def delete(self, *args, **kwargs):
|
|
order = self.order
|
|
super().delete(*args, **kwargs)
|
|
# Update the order total when an item is deleted
|
|
if order:
|
|
order.update_total()
|
|
|
|
class Meta:
|
|
verbose_name = 'Order Item'
|
|
verbose_name_plural = 'Order Items'
|
|
ordering = ['order']
|
|
|
|
@receiver(post_save, sender=Order)
|
|
def update_order_status(sender, instance, created, **kwargs):
|
|
"""When an order status changes to Delivered, update product stock"""
|
|
if not created and instance.status == 'Delivered':
|
|
for item in instance.order_items.all():
|
|
product = item.product
|
|
# Only update stock if we haven't already updated it
|
|
if product.stock >= item.quantity:
|
|
product.stock -= item.quantity
|
|
product.save()
|
|
|
|
class Cart(models.Model):
|
|
user = models.OneToOneField(User, related_name='cart', on_delete=models.CASCADE)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f"Cart for {self.user.username}"
|
|
|
|
def get_total(self):
|
|
"""Calculate total price of all items in cart"""
|
|
return sum(item.get_subtotal() for item in self.items.all())
|
|
|
|
def add_product(self, product, quantity=1):
|
|
"""Add a product to the cart"""
|
|
if quantity <= 0:
|
|
return None
|
|
|
|
cart_item, created = CartItem.objects.get_or_create(
|
|
cart=self,
|
|
product=product,
|
|
defaults={'quantity': quantity}
|
|
)
|
|
|
|
if not created:
|
|
cart_item.quantity += quantity
|
|
cart_item.save()
|
|
|
|
return cart_item
|
|
|
|
def remove_product(self, product):
|
|
"""Remove a product from the cart"""
|
|
try:
|
|
cart_item = CartItem.objects.get(cart=self, product=product)
|
|
cart_item.delete()
|
|
return True
|
|
except CartItem.DoesNotExist:
|
|
return False
|
|
|
|
def clear(self):
|
|
"""Remove all items from the cart"""
|
|
self.items.all().delete()
|
|
|
|
class Meta:
|
|
verbose_name = 'Cart'
|
|
verbose_name_plural = 'Carts'
|
|
ordering = ['-updated_at']
|
|
|
|
class CartItem(models.Model):
|
|
cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE)
|
|
product = models.ForeignKey(Product, related_name='cart_items', on_delete=models.CASCADE)
|
|
quantity = models.PositiveIntegerField(default=1)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.quantity} x {self.product.name} in {self.cart}"
|
|
|
|
def get_subtotal(self):
|
|
"""Calculate the subtotal for this item"""
|
|
return self.product.price * self.quantity
|
|
|
|
class Meta:
|
|
verbose_name = 'Cart Item'
|
|
verbose_name_plural = 'Cart Items'
|
|
ordering = ['-updated_at']
|
|
unique_together = ['cart', 'product']
|
|
|
|
@receiver(post_save, sender=User)
|
|
def create_user_cart(sender, instance, created, **kwargs):
|
|
"""Create a cart for new users"""
|
|
if created:
|
|
Cart.objects.create(user=instance)
|
|
|
|
class Favorite(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='favorites')
|
|
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='favorites')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ['user', 'product']
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username}'s favorite: {self.product.name}" |