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.
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
from django.contrib.gis.geos import Point
|
|
from rest_framework import serializers
|
|
|
|
from apps.core.media import presigned_media_url
|
|
from apps.locations.models import City, Neighborhood
|
|
from apps.locations.serializers import CitySerializer, NeighborhoodSerializer
|
|
|
|
from .models import Store, StoreBankAccount, StoreCategory, StoreWorkingHours
|
|
|
|
|
|
class StoreCategorySerializer(serializers.ModelSerializer):
|
|
icon_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = StoreCategory
|
|
fields = ('uuid', 'name', 'icon', 'icon_url', 'order')
|
|
|
|
def get_icon_url(self, obj):
|
|
return presigned_media_url(obj.icon)
|
|
|
|
|
|
class StoreWorkingHoursSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = StoreWorkingHours
|
|
fields = ('weekday', 'opens_at', 'closes_at', 'is_closed')
|
|
|
|
|
|
class StoreListSerializer(serializers.ModelSerializer):
|
|
category = StoreCategorySerializer(read_only=True)
|
|
logo_url = serializers.SerializerMethodField()
|
|
cover_image_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Store
|
|
fields = (
|
|
'uuid', 'name', 'category', 'logo', 'logo_url', 'cover_image', 'cover_image_url',
|
|
'rating_avg', 'rating_count', 'min_order_amount', 'delivery_fee',
|
|
'free_delivery_threshold', 'is_open',
|
|
)
|
|
|
|
def get_logo_url(self, obj):
|
|
return presigned_media_url(obj.logo)
|
|
|
|
def get_cover_image_url(self, obj):
|
|
return presigned_media_url(obj.cover_image)
|
|
|
|
|
|
class StoreDetailSerializer(StoreListSerializer):
|
|
working_hours = StoreWorkingHoursSerializer(many=True, read_only=True)
|
|
|
|
class Meta(StoreListSerializer.Meta):
|
|
fields = StoreListSerializer.Meta.fields + (
|
|
'description', 'address', 'phone_number',
|
|
'accepts_wallet', 'accepts_online', 'accepts_cash_on_delivery', 'working_hours',
|
|
)
|
|
|
|
|
|
class TopProductSerializer(serializers.Serializer):
|
|
name = serializers.CharField()
|
|
units_sold = serializers.IntegerField()
|
|
revenue = serializers.IntegerField()
|
|
|
|
|
|
class SalesChartPointSerializer(serializers.Serializer):
|
|
bucket = serializers.DateTimeField()
|
|
sales = serializers.IntegerField()
|
|
|
|
|
|
class SellerAnalyticsSerializer(serializers.Serializer):
|
|
period = serializers.CharField()
|
|
order_count = serializers.IntegerField()
|
|
total_sales = serializers.IntegerField()
|
|
cancellation_rate = serializers.FloatField()
|
|
average_order_value = serializers.IntegerField()
|
|
chart = SalesChartPointSerializer(many=True)
|
|
top_products = TopProductSerializer(many=True)
|
|
|
|
|
|
class StoreBankAccountSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = StoreBankAccount
|
|
fields = ('iban', 'account_holder_name')
|
|
|
|
|
|
class SellerStoreSerializer(serializers.ModelSerializer):
|
|
"""Read/write serializer for the authenticated seller's own store."""
|
|
|
|
category = StoreCategorySerializer(read_only=True)
|
|
category_uuid = serializers.PrimaryKeyRelatedField(
|
|
source='category', queryset=StoreCategory.objects.all(), write_only=True,
|
|
)
|
|
city = CitySerializer(read_only=True)
|
|
city_uuid = serializers.PrimaryKeyRelatedField(
|
|
source='city', queryset=City.objects.filter(is_active=True), write_only=True,
|
|
)
|
|
service_neighborhoods = NeighborhoodSerializer(many=True, read_only=True)
|
|
service_neighborhood_uuids = serializers.PrimaryKeyRelatedField(
|
|
source='service_neighborhoods', queryset=Neighborhood.objects.filter(is_active=True),
|
|
write_only=True, many=True, required=False,
|
|
)
|
|
latitude = serializers.FloatField(write_only=True, required=False)
|
|
longitude = serializers.FloatField(write_only=True, required=False)
|
|
bank_account = StoreBankAccountSerializer(read_only=True)
|
|
logo_url = serializers.SerializerMethodField()
|
|
cover_image_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Store
|
|
fields = (
|
|
'uuid', 'name', 'category', 'category_uuid', 'description', 'logo', 'logo_url',
|
|
'cover_image', 'cover_image_url',
|
|
'phone_number', 'city', 'city_uuid', 'address', 'latitude', 'longitude',
|
|
'service_neighborhoods', 'service_neighborhood_uuids', 'delivery_radius_km',
|
|
'min_order_amount', 'delivery_fee', 'free_delivery_threshold',
|
|
'accepts_wallet', 'accepts_online', 'accepts_cash_on_delivery',
|
|
'rating_avg', 'rating_count', 'status', 'is_open', 'bank_account', 'created_at',
|
|
)
|
|
read_only_fields = ('rating_avg', 'rating_count', 'status')
|
|
|
|
def get_logo_url(self, obj):
|
|
return presigned_media_url(obj.logo)
|
|
|
|
def get_cover_image_url(self, obj):
|
|
return presigned_media_url(obj.cover_image)
|
|
|
|
def _pop_location(self, validated_data):
|
|
lat = validated_data.pop('latitude', None)
|
|
lng = validated_data.pop('longitude', None)
|
|
if lat is not None and lng is not None:
|
|
validated_data['location'] = Point(lng, lat, srid=4326)
|
|
|
|
def create(self, validated_data):
|
|
self._pop_location(validated_data)
|
|
validated_data['owner'] = self.context['request'].user
|
|
return super().create(validated_data)
|
|
|
|
def update(self, instance, validated_data):
|
|
self._pop_location(validated_data)
|
|
return super().update(instance, validated_data)
|