I have to save images with s3 when creating a new object for my website.
I have a code to save my images that I use in my model's save method.
When I use the django administration panel, I can easily create my object without any error.
It also works well when I send my data without adding an image.
But when I try to create my object throught my view, I get this error : Error: 'duplicate key value violates unique constraint « store_shop_pkey »' DETAIL: key (id)=(37) already exists
I think that in my serializer's create method, I try to save my object twice : once for the image and once for the rest of my object's keys.
I don't know how to resolve this issue.
It works well with PUT method.
Here is my code :
models.py :
class Shop(models.Model):
name = models.CharField(max_length=255)
category = models.ForeignKey(ShopCategory, on_delete=models.SET_NULL, null=True, blank=True)
description = models.TextField(blank=True, null=True)
path = models.CharField(max_length=255, unique=True, null=True, blank=True) # Set a null and blank = True for serializer
mustBeLogged = models.BooleanField(default=False)
deliveries = models.FloatField(validators=[MinValueValidator(0),], default=7)
message = models.TextField(null=True, blank=True)
banner = models.ImageField(null=True, blank=True)
def save(self, *args, **kwargs):
try:
"""If we want to update"""
this = Shop.objects.get(id=self.id)
if self.banner:
image_resize(self.banner, 300, 300)
if this.banner != self.banner:
this.banner.delete(save=False)
else:
this.banner.delete(save=False)
except:
"""If we want to create a shop"""
if self.banner:
image_resize(self.banner, 300, 300)
super().save(*args, **kwargs)
def delete(self):
self.banner.delete(save=False)
super().delete()
def __str__(self):
return self.name
utils.py :
def image_resize(image, width, height):
# Open the image using Pillow
img = Image.open(image)
# check if either the width or height is greater than the max
if img.width > width or img.height > height:
output_size = (width, height)
# Create a new resized “thumbnail” version of the image with Pillow
img.thumbnail(output_size)
# Find the file name of the image
img_filename = Path(image.file.name).name
# Spilt the filename on “.” to get the file extension only
img_suffix = Path(image.file.name).name.split(".")[-1]
# Use the file extension to determine the file type from the image_types dictionary
img_format = image_types[img_suffix]
# Save the resized image into the buffer, noting the correct file type
buffer = BytesIO()
img.save(buffer, format=img_format)
# Wrap the buffer in File object
file_object = File(buffer)
# Save the new resized file as usual, which will save to S3 using django-storages
image.save(img_filename, file_object)
views.py :
class ShopList(ShopListView):
"""Create shop"""
def post(self, request):
"""For admin to create shop"""
serializer = MakeShopSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
serializers.py :
class MakeShopSerializer(serializers.ModelSerializer):
class Meta:
model = Shop
fields = '__all__'
def create(self, validated_data):
# validated_data.pop('path')
path = validated_data["name"].replace(" ", "-").lower()
path = unidecode.unidecode(path)
unique = False
while unique == False:
if len(Shop.objects.filter(path=path)) == 0:
unique = True
else:
# Generate a random string
char = "abcdefghijklmnopqrstuvwxyz"
path += "-{}".format("".join(random.sample(char, 5)))
shop = Shop.objects.create(**validated_data, path=path)
shop.save()
return shop
def update(self, instance, validated_data):
#You will have path in validated_data
#And you may have to check if the values are null
return super(MakeShopSerializer, self).update(instance, validated_data)
Finally, this is the data that I send :
Thank you by advance for your help
The next line is possible to have a problem with primary key:
shop = Shop.objects.create(**validated_data, path=path)
you may try to put every single attribute one by one, like this:
shop = Shop.objects.create(
name=validated_data['name'],
category=validated_data['category'],
...
path=path)
If it didn't resolve your problem, please post more about your error from terminal
Your model's unique key is path, so inside create function of your serializer,
shop = Shop.objects.create(**validated_data, path=path), tries creating a new model.
Consider the second instance you trying to add has the same path as the previous instance, in that case, you get this error since path should be unique and you are trying to add another model with the same value which DBMS rejects.
One of the things you can try is,
create or update the instance. If you are fine with updating the instance when your second instance has the same path as the previous one, then use
Shop.objects.update_or_create(path=path,
defaults=validated_data)
else
try adding unique constraints if your model cannot stand unique just by using the path field.
django documentation
Related
I was trying to make it this way but instead of {{product.id}}, folder is named None.
I read some articles about that, and I found out that is because folder is being made before whole object. So how should I do that to make it work?
models.py:
def getImageURL(self, filename):
return "products_images/" + str(self.pk) + "/product_image.png"
class Product(models.Model):
name = models.CharField(max_length=200)
image = models.ImageField(upload_to=getImageURL, default="media/static/products_images/default.png" )
changed my funtion to:
def getImageURL(instance, filename):
return "products_images/{}/product_image.png".format(instance.id)
But it works only while edditing existing object. When I'm trying to make a new one id is set to None.
Edit:
ok, finally I did it this way:
def getFileNumber():
queryset = Product.objects.all().order_by('pk')
last = queryset.last()
last_id = last.id
file_number = last_id+1
return str(file_number)
def getImageURL(instance, filename):
return "products_images/{}/product_image.png".format(getFileNumber())
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
availability = models.IntegerField()
price = models.DecimalField(max_digits=5, decimal_places=2)
Probably it is not the best way to do that but it's very simple to understand so why not to use it.
Some debugging:
def getFileNumber():
queryset = Product.objects.all().order_by('pk')
if queryset:
last = queryset.last()
last_id = last.id
file_number = last_id+1
return str(file_number)
else:
file_number=1
return str(file_number)
def getImageURL(instance, filename):
path = os.path.join("products_images/{}/product_image.png".format(getFileNumber()))
print(path)
if os.path.isfile("media/" + path):
print("image with this id already exist, ")
os.remove("media/" + path)
return path
else:
return path
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
availability = models.IntegerField()
price = models.DecimalField(max_digits=5, decimal_places=2)
The following is the issue:
The instance doesn't have a primary key yet before it is created
When the instance gets saved in the database, then you can get the primary key
Maybe use signals, somehow? Do the logic it in the view after saving it?
Alternative option to pk:
You can generate a UUID for the object.
id = models.UUIDField(
primary_key = True,
default = uuid.uuid4,
editable = False)
Or alternatively:
Have a look at this package, you can set the uuid alphabet, length, and some more. This would allow you access to the id, for sure (same with uuid).
https://pypi.org/project/shortuuid/
id = ShortUUIDField(
length=16,
max_length=40,
prefix="id_",
alphabet="abcdefg1234",
primary_key=True,
)
Sidenote:
def getImageURL(self, filename): #Self would normally be defined as instance, but I suppose this is just semantics.
This is because the id is saved to the model after calling save(). The first guess would be to use save() to get the object in return. But save() does not return anything. So I put together a little example
class Thingy(models.Model):
name = models.CharField(
_('name'),
max_length=64,
)
def save(self, *args, **kwargs):
super(Thingy, self).save(*args, **kwargs)
print(self.pk)
My test was:
>>> t = Thingy(name="lol")
>>> t.save()
1
And it printed the primary key since save() mutades self. You could check if the pk exists before save(). If yes, just add the image path. If not. Execute save() first, manipulate the image path using pk, and save the object again. It is not elegant in any way, but I guess that is the only way.
I implemented a dependent dropdown on my Django webapp. I used this tutorial Implementing dependent drop down. However, challenge comes when I want to update the form. To put this in perspective, let me recreate the code here.
Model.py
class VehicleMake(models.Model):
make = models.CharField(max_length=20)
manufacturer = models.CharField(max_length=20)
def __str__(self):
return self.make
class VehicleModel(models.Model):
make = models.ForeignKey(VehicleMake, on_delete=models.CASCADE)
model_name = models.CharField(max_length=20)
def __str__(self):
return self.model_name
class Vehicle(models.Model):
model = models.ForeignKey(VehicleModel, on_delete=models.CASCADE)
description = models.TextField()
Notice that unlike in the provided tutorial, I don't have both of the dependent fields on the vehicle model. That is to avoid repetition since if you know the vehicle model, you will definitely know the make from the VehicleModel table.
Here is the form:
forms.py
class VehicleDetails(forms.ModelForm):
make = forms.ModelChoiceField(queryset=VehicleMake.objects.all(),
empty_label="Select Vehicle Make")
class Meta:
model = Vehicle
fields = ['make', 'model', 'description'
]
def __init__(self, *args, **kwargs):
super(VehicleDetails, self).__init__(*args, **kwargs)
self.fields['model'].queryset = VehicleModel.objects.none()
if 'make' in self.data:
try:
make = int(self.data.get('make'))
self.fields['model'].queryset = VehicleModel.objects.filter(make=make).order_by('model_name')
except (ValueError, TypeError):
pass # invalid input from the client; ignore and fallback to empty VehicleModel queryset
elif self.instance.pk:
vehicle_model = VehicleModel.objects.get(self.instance.model)
self.fields['make'] = vehicle_model.make
self.fields['model'].queryset = self.instance.model_set.filter(make=vehicle_model.make).order_by('model_name')
So, my challenge is, when I want to update the form, I get an error from the last section of the form under the elif code. I want to get the value of make using the store value of model then use that to render the form of course with the select of options of model being those from the selected make, unless the user now makes changes to the make field.
This is what I have tried so far (especially under the elif section on the forms.py) but I keep getting the error: TypeError: 'VehicleModel' object is not iterable. What am I doing wrong?
I was able to solve this by changing this in the elif block:
vehicle_model = VehicleModel.objects.get(pk=self.instance.model.id)
self.fields['make'].queryset = self.instance.model.make.__class__.objects.all()
self.fields['model'].queryset = self.instance.model.__class__.objects.filter(make=vehicle_model.make).order_by('model_name')
Then all I had to do at the views.py to ensure that current value is loaded was to add a initial value while loading the form, i.e.
vehicle_form = VehicleDetails(instance=listing.vehicle, initial = {'make': listing.vehicle.model.make })
I hope it helps anyone in the same problem.
I made image gallery with grid view, but I don't like the way that rows look like - vertical photos disrupt everything. As I don't want to manually change images order I'm looking for a way to sort them automaticaly by image height or just image orientation, so vertical photos go to the bottom in one row.
Thats how my model in Django looks like:
class Photo(models.Model):
title = models.CharField(max_length=150)
image = models.ImageField()
description = models.TextField(blank=True)
category = models.IntegerField(choices=CATEGORIES)
published = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.title
Here is my grid_view:
def photos_grid(request):
global cat_list
photos = Photo.objects.order_by('published')
output = {'photos': photos, 'categories': cat_list,}
return render(request, 'photos/photos_grid.html', output)
I tried that (how to find height and width of image for FileField Django) way of getting image dimensions but I got
ValueError: invalid literal for int() with base 10: 'height'
every way I try to put it in my code. Other idea (by getting manually dimensions in views.py) works, but I can't put it together with my photos on the list so it get sorted.
You must include the height and width fields in your model, for example:
class Photo(models.Model):
image = models.ImageField(height_field='image_height', width_field='image_width')
image_height = models.IntegerField()
image_width = models.IntegerField()
...
After migrating your database, you can write the following:
Photo.objects.all().order_by('image_height')
Edit: If you need to access the orientation, add another field, such as:
class Photo(models.Model):
...
aspect_ratio = models.FloatField(blank=True, null=True)
then, override your save method to populate this field using height and width, i.e.:
class Photo(models.Model):
...
def save(self, **kwargs):
self.aspect_ratio = float(self.image_height) / float(self.image_width)
super(Photo, self).save(kwargs)
You can then order by your new field, such as:
Photo.objects.all().order_by('aspect_ratio')
my problem is regarding orphaned files and the fact that my model uses several file fields. I have a model for a user, which has a profile image field and a cv field. The user is able to update these fields which works fine but the old files remain. Now I've looked at various solutions online, created my own etc but its not working quite properly. The problem with my solution (see below) is that it deletes all old media files associated with that instance at that time even though I've only changed one value. Additionally, each of the fields have their own directory path so its not a case of recursive deletion due to sharing the same directory path. Its fine when I change both fields since both change and the previous files are deleted but often people only change one value so how do I fix this? I don't want to use django-cleanup for a variety of reasons.
models.py
def teacher_avatar_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'teachers/avatar/{0}/{1}'.format(instance.user.email.replace(" ", "_").lower(), filename)
def teacher_cv_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'teachers/cv/{0}/{1}'.format(instance.user.email.replace(" ", "_").lower(), filename)
#Teacher
class ProfileTeacher(models.Model):
created = models.DateTimeField(auto_now=False, auto_now_add=True, blank = False, null = False, verbose_name = 'Creation Date')
user = models.OneToOneField(app_settings.USER_MODEL,blank=True, null=False)
first_name = models.CharField(max_length = 400, null=True, blank = True, verbose_name = 'First Name')
last_name = models.CharField(max_length = 400, null=True, blank = True, verbose_name = 'Surname')
phone_number = models.CharField(max_length = 15, null=True, blank = True, verbose_name = 'Phone Number')
cvv = models.FileField(upload_to=teacher_cv_directory_path, null=True, blank = True, verbose_name="CV")
profile_image = models.ImageField(upload_to=teacher_avatar_directory_path,
null = True,
blank=True,
default='/perfil.png',
)
#-------Model image and files clean up ----------------------------------
#receiver(post_init, sender= ProfileTeacher)
def backup_image_and_cv_paths(sender, instance, **kwargs):
instance._current_image_file = instance.profile_image
instance._current_cvv_file = instance.cvv
#receiver(post_save, sender= ProfileTeacher)
def delete_old_image(sender, instance, **kwargs):
if hasattr(instance, '_current_image_file'):
if instance._current_image_file != instance.profile_image.path:
instance._current_image_file.delete(save=False)
if hasattr(instance, '_current_cvv_file'):
if instance._current_cvv_file != instance.cvv.path:
instance._current_cvv_file.delete(save=False)
You can make custom save method for your Model. Docs here
class YourModel(models.Model):
field_1 = ...
field_2 = ...
def save(self, *args, **kw):
if self.id:
# Check that instance that you are sending for update have files in it
# Get corresponding file field(s) and manually remove old file(s) according to the path
# Store new file(s) to the file field(s)
# Update other fields if required
super().save(*args, **kwargs)
I have a model like this:
class Item(models.Model):
code = models.CharField(max_length=200, unique=True)
barcode = models.CharField(max_length=300)
desc = models.CharField('Description',max_length=500)
display_desc = models.CharField('Description',max_length=500,
blank=True, null=True)
price = models.FloatField()
discountable = models.BooleanField(blank=True, default=False)
image_path = models.CharField(max_length=300,unique=True, blank=True, null=True)
def __unicode__(self):
return self.code + ' : ' + self.desc
But unfortunately, I don't want to store the item's image in the database, instead I want to store the image path in the server in the image_path column.
So, I created a custom admin.py for this object so that I could edit/insert the object thru the Django admin module. As a result, below is the customized admin.py
class ItemAdminForm(forms.ModelForm):
file_upload = forms.FileField(required=False)
class Meta:
model = Item
def __init__(self, *args, **kwargs):
super(ItemAdminForm, self).__init__(*args,**kwargs)
#if kwargs.has_key('instance'):
# instance = kwargs['instance']
# self.initial['file_upload'] = instance.file_upload
def handle_uploaded_file(f):
destination = open('D:/Project/pensel/penselshop/static/picture', 'wb+')
for chunk in f.chunks():
destination.write(chunk)
destination.close()
return f.name
def save(self,commit=True):
name = None
extension = None
#error here! How could I get the request?
miniform = ItemAdminForm(request.POST, request.FILES)
if miniform.is_valid():
name = handle_uploaded_file(request.FILES['file_upload'])
extension = name.split('.')[1]
model = super(ItemAdminForm, self).save(commit=False)
model.image_path = '/static/picture/' + model.code + extension
if commit:
model.save()
return model
However, during processing the save() function, I noticed that there is an error in getting the request. How can I get the request so that I could retrieve the file? I noticed that the request is automatically added in views.py, but not admin.py
Django's ImageField and FileField fields don't actually store the image in the database either. All that is stored in the database is the path, which you can control yourself. The actual image file is stored on the filesystem. So I'm not sure why you are going to all this trouble...?
But to answer your question of how to get the request in the admin, you can override ModelAdmin.save_model().