Files

587 lines
20 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
import logging
logger = logging.getLogger(__name__)
GENDER_CHOICES = (("Male", "Male"), ("Female", "Female"), ("Other", "Other"))
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)
gender = models.CharField(max_length=10, choices=GENDER_CHOICES, default="Male", blank=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)
profile_picture = models.ImageField(upload_to='profile_pictures/', 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()
else:
# Log error if stock is insufficient
logger.error(f"Insufficient stock for product {product.name} when delivering order {instance.id}")
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}"
class PrivacyPolicy(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
last_updated = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.title
class Meta:
verbose_name_plural = "Privacy Policies"
class FAQ(models.Model):
CATEGORY_CHOICES = (
('All', 'All'),
('Services', 'Services'),
('General', 'General'),
('Account', 'Account')
)
question = models.CharField(max_length=255)
answer = models.TextField()
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='General')
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.question
class Meta:
verbose_name_plural = "FAQs"
ordering = ['-created_at']
class Contact(models.Model):
CONTACT_TYPES = [
('customer_service', 'Customer Service'),
('whatsapp', 'WhatsApp'),
('website', 'Website'),
('facebook', 'Facebook'),
('instagram', 'Instagram'),
('twitter', 'Twitter'),
]
contact_type = models.CharField(max_length=20, choices=CONTACT_TYPES)
title = models.CharField(max_length=100)
value = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['contact_type']
def __str__(self):
return f"{self.get_contact_type_display()} - {self.title}"
class Slider(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
image = models.ImageField(upload_to='sliders/', blank=True, null=True)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, blank=True, help_text="Category to show when clicking the 'Show Now' button")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class Meta:
ordering = ['-created_at']
@property
def image_url(self):
if self.image:
return f"/media/{self.image.name}"
return None
class Coupon(models.Model):
code = models.CharField(max_length=50, unique=True)
discount_type = models.CharField(
max_length=10,
choices=[
('percentage', 'Percentage'),
('fixed', 'Fixed Amount')
],
default='percentage'
)
discount_value = models.DecimalField(max_digits=10, decimal_places=2)
min_purchase = models.DecimalField(max_digits=10, decimal_places=2, default=0)
max_discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
is_active = models.BooleanField(default=True)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.code} - {self.discount_value}{'%' if self.discount_type == 'percentage' else '$'}"
class Meta:
verbose_name = "Coupon"
verbose_name_plural = "Coupons"
ordering = ['-created_at']
def is_valid(self):
from django.utils import timezone
now = timezone.now()
return self.is_active and self.start_date <= now <= self.end_date
def calculate_discount(self, total_amount):
if not self.is_valid() or total_amount < self.min_purchase:
return 0
if self.discount_type == 'percentage':
discount = (total_amount * self.discount_value) / 100
if self.max_discount:
discount = min(discount, self.max_discount)
return discount
else:
return self.discount_value
class Notification(models.Model):
NOTIFICATION_TYPES = (
('order_placed', 'Order Placed'),
('order_shipped', 'Order Shipped'),
('order_delivered', 'Order Delivered'),
('order_cancelled', 'Order Cancelled'),
('coupon', 'Coupon'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
message = models.TextField()
read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
order = models.ForeignKey('Order', on_delete=models.SET_NULL, null=True, blank=True)
coupon = models.ForeignKey('Coupon', on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.get_type_display()} - {self.user.username}"
@receiver(post_save, sender=Order)
def create_order_notification(sender, instance, created, **kwargs):
"""Create notifications when orders are created or updated"""
if created:
# Create notification for new order
Notification.objects.create(
user=instance.user,
type='order_placed',
message=f'Your order #{instance.id} has been placed successfully',
order=instance
)
else:
# Create notifications for order status changes
if instance.status == 'Shipped':
Notification.objects.create(
user=instance.user,
type='order_shipped',
message=f'Your order #{instance.id} has been shipped',
order=instance
)
elif instance.status == 'Delivered':
Notification.objects.create(
user=instance.user,
type='order_delivered',
message=f'Your order #{instance.id} has been delivered',
order=instance
)
elif instance.status == 'Cancelled':
Notification.objects.create(
user=instance.user,
type='order_cancelled',
message=f'Your order #{instance.id} has been cancelled',
order=instance
)
@receiver(post_save, sender=Coupon)
def create_coupon_notification(sender, instance, created, **kwargs):
"""Create notifications when new coupons are created"""
if created and instance.is_active:
# Get all active users
users = User.objects.filter(is_active=True)
# Create notifications for all users
for user in users:
Notification.objects.create(
user=user,
type='coupon',
message=f'New coupon code {instance.code} available! Get {instance.discount_value}{"%" if instance.discount_type == "percentage" else "$"} off',
coupon=instance
)