Skip to content

Commit a133483

Browse files
Merge pull request #397 from MySecondLanguage/fixed-variant-image-upload
Fixed variant image upload
2 parents 4c45ca4 + ed37e3f commit a133483

File tree

11 files changed

+173
-19
lines changed

11 files changed

+173
-19
lines changed

nxtbn/admin_schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from nxtbn.cart.admin_query import AdminCartQuery
44
from nxtbn.core.admin_mutation import CoreMutation
55
from nxtbn.core.admin_queries import AdminCoreQuery
6+
from nxtbn.filemanager.admin_queries import ImageQuery
67
from nxtbn.order.admin_mutation import AdminOrderMutation
78
from nxtbn.payment.admin_queries import AdminPaymentQuery
89
from nxtbn.product.admin_mutations import ProductMutation
@@ -15,7 +16,7 @@
1516

1617

1718

18-
class Query(ProductQuery, AdminOrderQuery, AdminCoreQuery, WarehouseQuery, AdminCartQuery, UserAdminQuery, PurchaseQuery, AdminPaymentQuery):
19+
class Query(ProductQuery, AdminOrderQuery, AdminCoreQuery, WarehouseQuery, AdminCartQuery, UserAdminQuery, PurchaseQuery, AdminPaymentQuery, ImageQuery):
1920
pass
2021

2122
class Mutation(AdminUserMutation, ProductMutation, CoreMutation, AdminOrderMutation):

nxtbn/filemanager/admin_queries.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.conf import settings
2+
import graphene
3+
from graphql import GraphQLError
4+
from graphene_django.filter import DjangoFilterConnectionField
5+
from nxtbn.filemanager.models import Image
6+
from nxtbn.filemanager.admin_types import ImageType
7+
8+
9+
class ImageQuery(graphene.ObjectType):
10+
images = DjangoFilterConnectionField(ImageType)
11+
image = graphene.Field(ImageType, id=graphene.ID(required=True))
12+
13+
def resolve_images(self, info, **kwargs):
14+
return Image.objects.all().order_by('-created_at')
15+
16+
def resolve_image(self, info, id):
17+
try:
18+
return Image.objects.get(id=id)
19+
except Image.DoesNotExist:
20+
return None

nxtbn/filemanager/admin_types.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from django.conf import settings
2+
import graphene
3+
from graphene_django import DjangoObjectType
4+
from graphene import relay
5+
from nxtbn.filemanager.models import Image
6+
7+
8+
class ImageType(DjangoObjectType):
9+
db_id = graphene.Int(source='id')
10+
image = graphene.String()
11+
image_xs = graphene.String()
12+
13+
def resolve_image(self, info):
14+
return self.get_image_url(info.context)
15+
16+
def resolve_image_xs(self, info):
17+
return self.get_image_xs_url(info.context)
18+
19+
class Meta:
20+
model = Image
21+
fields = (
22+
'id',
23+
'name',
24+
'image',
25+
'image_xs',
26+
'created_at',
27+
'last_modified',
28+
)
29+
interfaces = (relay.Node, )
30+
filter_fields = {
31+
'name': ['exact', 'icontains'],
32+
}

nxtbn/filemanager/api/dashboard/serializers.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
from django.utils.translation import gettext_lazy as _
22
from rest_framework import serializers
3+
from nxtbn import settings
34
from nxtbn.filemanager.models import Image, Document
45

56

67
from PIL import Image as PILImage
78
from io import BytesIO
89
from django.core.files.base import ContentFile
910

11+
1012
class ImageSerializer(serializers.ModelSerializer):
1113
class Meta:
1214
model = Image
1315
fields = "__all__"
14-
read_only_fields = (
15-
"id",
16-
"created_by",
17-
"last_modified_by",
18-
)
16+
read_only_fields = ("id", "created_by", "last_modified_by")
1917

2018
def create(self, validated_data):
2119
request = self.context["request"]
@@ -25,6 +23,7 @@ def create(self, validated_data):
2523
# Optimize the image before saving
2624
if "image" in validated_data:
2725
validated_data["image"] = self.optimize_image(validated_data["image"])
26+
validated_data["image_xs"] = self.optimize_image(validated_data["image"], max_size_kb=1, format="png", max_dimension=50)
2827

2928
return super().create(validated_data)
3029

@@ -39,28 +38,38 @@ def update(self, instance, validated_data):
3938
return super().update(instance, validated_data)
4039

4140
@staticmethod
42-
def optimize_image(image_file, max_size_kb=200, format="WEBP"):
41+
def optimize_image(image_file, max_size_kb=settings.IMAGE_COMPRESS_MAX, format="WEBP", max_dimension=800):
4342
"""
4443
Optimize an image by resizing and converting it to the specified format while maintaining the aspect ratio.
4544
Ensures the image file size is below max_size_kb.
4645
"""
4746
img = PILImage.open(image_file)
48-
img = img.convert("RGB") # Ensure compatibility for formats like PNG with alpha
47+
img = img.convert("RGB") # Convert to RGB for compatibility
48+
49+
# Adjust dimension based on image type (smaller for PNG to ensure < 2023 bytes)
50+
if format.lower() == "png":
51+
max_dimension = 64 # Reduce size for PNG to keep it small
4952

50-
# Resize the image to fit within a reasonable dimension (adjust if needed)
51-
max_dimension = 800
5253
img.thumbnail((max_dimension, max_dimension), PILImage.Resampling.LANCZOS)
5354

54-
# Save the image to a buffer
55+
# Use a buffer to store the optimized image
5556
buffer = BytesIO()
56-
img.save(buffer, format=format, optimize=True, quality=85)
57-
buffer.seek(0)
5857

59-
# Check if the image exceeds the maximum size, reduce quality iteratively
60-
while buffer.tell() > max_size_kb * 1024:
61-
buffer.seek(0)
62-
img.save(buffer, format=format, optimize=True, quality=max(10, 85 - 10))
63-
buffer.seek(0)
58+
if format.lower() == "png":
59+
# Reduce colors using quantization (important for PNG)
60+
img = img.convert("P", palette=PILImage.ADAPTIVE, colors=32) # Limit colors to 32
61+
img.save(buffer, format="PNG", optimize=True)
62+
else:
63+
quality = 85
64+
img.save(buffer, format=format, optimize=True, quality=quality)
65+
66+
# Reduce quality iteratively for WebP/JPEG if size is too large
67+
while buffer.tell() > max_size_kb * 1024 and quality > 10:
68+
quality -= 5
69+
buffer = BytesIO() # Reset buffer
70+
img.save(buffer, format=format, optimize=True, quality=quality)
71+
72+
buffer.seek(0)
6473

6574
# Return the new image as a ContentFile
6675
return ContentFile(buffer.read(), name=f"{image_file.name.split('.')[0]}.{format.lower()}")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.11 on 2025-02-20 15:56
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("filemanager", "0002_initial"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="image",
15+
name="image_sm",
16+
field=models.ImageField(blank=True, null=True, upload_to=""),
17+
),
18+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.11 on 2025-02-20 16:00
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("filemanager", "0003_image_image_sm"),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name="image",
15+
old_name="image_sm",
16+
new_name="image_xs",
17+
),
18+
]

nxtbn/filemanager/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@ class Image(AbstractBaseModel):
99
last_modified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='image_modified', null=True, blank=True)
1010
name = models.CharField(max_length=255)
1111
image = models.ImageField()
12+
image_xs = models.ImageField(null=True, blank=True)
1213
image_alt_text = models.CharField(max_length=255)
1314

15+
def get_image_url(self,request):
16+
if self.image:
17+
return request.build_absolute_uri(self.image.url)
18+
return None
19+
20+
def get_image_xs_url(self,request):
21+
if self.image_xs:
22+
return request.build_absolute_uri(self.image_xs.url)
23+
24+
if self.image:
25+
return request.build_absolute_uri(self.image.url)
26+
27+
return None
28+
1429

1530
class Document(AbstractBaseModel):
1631
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='document_created')

nxtbn/product/admin_types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ class ProductVariantNonPaginatedType(DjangoObjectType):
1212
display_name = graphene.String()
1313
humanize_price = graphene.String()
1414
variant_thumbnail = graphene.String()
15+
variant_thumbnail_xs = graphene.String()
1516

1617
def resolve_humanize_price(self, info):
1718
return self.humanize_total_price()
1819

1920
def resolve_variant_thumbnail(self, info):
2021
return self.variant_thumbnail(info.context)
22+
23+
def resolve_variant_thumbnail_xs(self, info):
24+
return self.variant_thumbnail_xs(info.context)
2125
class Meta:
2226
model = ProductVariant
2327
fields = (
@@ -46,12 +50,16 @@ class ProductGraphType(DjangoObjectType):
4650
db_id = graphene.Int(source="id")
4751
all_variants = graphene.List(ProductVariantNonPaginatedType)
4852
product_thumbnail = graphene.String()
53+
product_thumbnail_xs = graphene.String()
4954

5055
def resolve_all_variants(self, info):
5156
return self.variants.all()
5257

5358
def resolve_product_thumbnail(self, info):
5459
return self.product_thumbnail(info.context)
60+
61+
def resolve_product_thumbnail_xs(self, info):
62+
return self.product_thumbnail_xs(info.context)
5563
class Meta:
5664
model = Product
5765
fields = (

nxtbn/product/api/dashboard/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class ProductVariantSerializer(serializers.ModelSerializer):
8484
is_default_variant = serializers.SerializerMethodField()
8585
product_name = serializers.SerializerMethodField()
8686
price = serializers.SerializerMethodField()
87+
image_details = ImageSerializer(read_only=True, source='image')
8788

8889
class Meta:
8990
model = ProductVariant
@@ -104,6 +105,7 @@ class Meta:
104105
'track_inventory',
105106
'is_default_variant',
106107
'allow_backorder',
108+
'image_details',
107109
)
108110

109111
def get_is_default_variant(self, obj):
@@ -162,6 +164,7 @@ class VariantCreatePayloadSerializer(serializers.Serializer):
162164
is_default_variant = serializers.BooleanField(default=False)
163165
track_inventory = serializers.BooleanField(default=False)
164166
allow_backorder = serializers.BooleanField(default=False)
167+
image = serializers.PrimaryKeyRelatedField(queryset=Image.objects.all(), required=False, allow_null=True)
165168
# def validate_sku(self, value):
166169
# if ProductVariant.objects.filter(sku=value).exists():
167170
# raise serializers.ValidationError("SKU already exists.")

nxtbn/product/models.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,18 @@ def product_thumbnail(self, request):
221221
full_url = request.build_absolute_uri(image_url)
222222
return full_url
223223
return None
224-
224+
225+
def product_thumbnail_xs(self, request):
226+
"""
227+
Returns the URL of the first image associated with the product.
228+
If no image is available, returns None.
229+
"""
230+
first_image = self.images.first() # Get the first image if it exists
231+
if first_image and hasattr(first_image, 'image_xs') and first_image.image_xs:
232+
image_url = first_image.image_xs.url
233+
full_url = request.build_absolute_uri(image_url)
234+
return full_url
235+
return None
225236

226237

227238

@@ -385,6 +396,24 @@ def variant_thumbnail(self, request):
385396
full_url = request.build_absolute_uri(image_url)
386397
return
387398
return None
399+
400+
def variant_thumbnail_xs(self, request):
401+
"""
402+
Returns the URL of the first image associated with the product variant.
403+
If no image is available, returns None.
404+
"""
405+
if self.image and hasattr(self.image, 'image_xs') and self.image.image_xs:
406+
image_url = self.image.image_xs.url
407+
full_url = request.build_absolute_uri(image_url)
408+
return full_url
409+
410+
if self.product.images.exists():
411+
first_image = self.product.images.first()
412+
if first_image and hasattr(first_image, 'image_xs') and first_image.image_xs:
413+
image_url = first_image.image_xs.url
414+
full_url = request.build_absolute_uri(image_url)
415+
return
416+
return None
388417

389418

390419
def get_valid_stock(self): # stocks that available for sell

0 commit comments

Comments
 (0)