The prior commit wired MinIO as Django's default storage backend but kept plain ImageField multipart uploads through the Django backend and public-bucket direct URLs — not how campaign/advertising/promotions do it. Rework to match: image fields (ProductCategory.icon, Product.image, StoreCategory.icon, Store.logo, Store.cover_image) now store an opaque MinIO object_key (CharField) instead of a Django-managed file. - utils/clients/minio_client.py — raw Minio SDK client, same shape as the other services. - apps/core/views/media.py — POST /api/media/presign/ mints a 30-minute presigned PUT URL + object_key; the client uploads bytes directly to MinIO, then submits the object_key on the owning resource. - apps/core/media.py — presigned_media_url() helper; every serializer with an image field now also exposes a `<field>_url` computed from a fresh 1-hour presigned GET, rather than trusting a stored/direct URL. - FRONTEND_GUIDE.md updated to document the new upload flow and field shapes (object_key vs `_url`), replacing the stale "no upload endpoint yet" note. Verified against a live local MinIO container: presign → direct PUT upload → object_key stored on the model → serializer mints a working presigned GET URL → URL fetches the uploaded bytes. Also exercised the actual DRF view (POST /api/media/presign/) end-to-end, including the 401 on an unauthenticated request.
60 lines
2.1 KiB
Python
60 lines
2.1 KiB
Python
from django.db import models
|
|
|
|
from apps.stores.models import Store
|
|
from utils.models import BaseModel
|
|
|
|
|
|
class ProductCategory(BaseModel):
|
|
name = models.CharField(max_length=100)
|
|
parent = models.ForeignKey(
|
|
'self', on_delete=models.CASCADE, related_name='children', null=True, blank=True,
|
|
)
|
|
icon = models.CharField(max_length=500, null=True, blank=True, help_text='MinIO object key.')
|
|
order = models.PositiveSmallIntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['order', 'name']
|
|
verbose_name = 'دستهبندی محصول'
|
|
verbose_name_plural = 'دستهبندیهای محصول'
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Product(BaseModel):
|
|
class UnitType(models.TextChoices):
|
|
GRAM = 'gram', 'گرم'
|
|
MILLILITER = 'ml', 'میلیلیتر'
|
|
PIECE = 'piece', 'عدد'
|
|
|
|
store = models.ForeignKey(Store, on_delete=models.CASCADE, related_name='products')
|
|
category = models.ForeignKey(ProductCategory, on_delete=models.PROTECT, related_name='products')
|
|
name = models.CharField(max_length=200)
|
|
description = models.TextField(blank=True)
|
|
image = models.CharField(max_length=500, null=True, blank=True, help_text='MinIO object key.')
|
|
|
|
price = models.PositiveBigIntegerField()
|
|
unit_type = models.CharField(max_length=10, choices=UnitType.choices, default=UnitType.PIECE)
|
|
unit_value = models.PositiveIntegerField(null=True, blank=True)
|
|
|
|
stock_quantity = models.PositiveIntegerField(default=0)
|
|
low_stock_threshold = models.PositiveIntegerField(default=5)
|
|
|
|
is_active = models.BooleanField(default=True)
|
|
sold_count = models.PositiveIntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
verbose_name = 'محصول'
|
|
verbose_name_plural = 'محصولات'
|
|
|
|
def __str__(self):
|
|
return f'{self.name} - {self.store.name}'
|
|
|
|
@property
|
|
def is_out_of_stock(self):
|
|
return self.stock_quantity <= 0
|
|
|
|
@property
|
|
def is_low_stock(self):
|
|
return 0 < self.stock_quantity <= self.low_stock_threshold
|