Hacking django-stdimage2 to support multiple images - django

I am working on a Django project in which users should be allowed to upload multiple images at once. That portion of the project will likely be handled by SWFUpload, unless you have a better suggestion.
The image renaming, resizing, and thumbnail creation will be handled by django-stdimage2.
The Problem
django-stdimage2 renames each image using the field name and object primary key.
If five images exist for gear row with primary key 1, all five images will be renamed "image_1.jpeg".
Before I introduce a possible solution, here are my models.
Basically, one gear row can have many gear_image rows.
class gear(models.Model):
id = models.AutoField(primary_key=True)
model = models.CharField(max_length=100)
class gear_images(models.Model):
id = models.AutoField(primary_key=True)
gear_id = models.ForeignKey(gear)
image = StdImageField(upload_to='images/gear', blank=True, size=(640, 480, True), thumbnail_size=(100, 100, True))
A Solution
I was thinking of adding a timestamp, in milliseconds and rounded, to the filename.
I'm neither a Python or Django pro but I poked around in django-stdimage2's fields.py file and I think I located the code I need to edit to make this work.
The two lines of code that are commented out are my proposed solutions:
def _rename_resize_image(self, instance=None, **kwargs):
'''
Renames the image, and calls methods to resize and create the thumbnail
'''
if not kwargs.get('raw', None):
if getattr(instance, self.name):
filename = getattr(instance, self.name).path
ext = os.path.splitext(filename)[1].lower().replace('jpg', 'jpeg')
# time_stamp = int(round(time.time() * 1000))
# dst = self.generate_filename(instance, '%s_%s_%s%s' % (self.name, instance._get_pk_val(), time_stamp, ext))
dst = self.generate_filename(instance, '%s_%s%s' % (self.name, instance._get_pk_val(), ext))
dst_fullpath = os.path.join(settings.MEDIA_ROOT, dst)
if os.path.normcase(os.path.abspath(filename)) != os.path.normcase(os.path.abspath(dst_fullpath)):
os.rename(filename, dst_fullpath)
if self.size:
self._resize_image(dst_fullpath, self.size)
if self.thumbnail_size:
thumbnail_filename = self._get_thumbnail_filename(dst_fullpath)
shutil.copyfile(dst_fullpath, thumbnail_filename)
self._resize_image(thumbnail_filename, self.thumbnail_size)
setattr(instance, self.attname, dst)
instance.save()
Each image name would look something like: image_1_159753456.jpeg
Do you think this is a good work-around? I am open to other ideas also.
Thank you :)

I do not think you have a problem here at all.
django-stdimage{2} will rename to the object id of your gear_images model, not its parent gear model. So one gear can have many images, each will have the gear_images pk appended to the filename.
So really, you only have a problem if it's important to you to use the gear model's pk in the filename instead of the gear_images pk. If you're ok with the latter, then you don't need to do anything, it should just work.

Related

How to format DurationField input as min:sec:millisec Django

I want to force users to input lap times into a Form using the format min:sec:millisec (e.g. 00:00:000). I also want to display these times in this format in a DetailView but I want to store them as milliseconds to calculate personal bests and lap differences.
I have tried to set the default DurationField value as 01:01:001 but it displays in the format HH:MM:SS.MS
Here is my model:
class SwimTime(models.Model):
swimmer =models.ForeignKey(Swimmer, on_delete=models.CASCADE)
time = models.DurationField(_('Time'), default= timedelta(minutes=1, seconds=1, milliseconds=1))
distance = models.PositiveIntegerField(_('Distance'),null = False, default=50)
strokeType = models.CharField(_('Stroke Type'),max_length=20, choices=strokeTypes, default='FC')
date = models.DateField(_('Date Recorded'),default = timezone.now)
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)
If you want to have an "example" you can use help_text option for the field showing the expected input format. You can use it as any other option: default, null...
help_text="Please use the following format: YYYY-MM-DD."
Anyway, this has nothing to do with how it will be rendered in any template or even in database or browser validation.
For templates you can use the Datetime formatting. Django has not built-in formatting as it has for date and time, but there are some projects that solve that. Also, in this question there are some good examples for writing your own filters and load them in the template.
Also, reading your data I guess that null=False is not necessary in 'distance' field: by default it will be set to False. And keep in mind that null=True and blank=True have different uses.

Django: Uploaded Filename Issue

In my django app i'm trying to resize & compress an Image before saving it to the database.
Here's how I did it inside the models
class Data(models.Model):
image = models.ImageField(upload_to='all_images/', null=True, blank=True)
def save(self, *args, **kwargs):
if self.image:
img = Image.open(self.image)
resize = img.resize((240, 240), Image.ANTIALIAS)
new_image = BytesIO()
resize.save(new_image, format=img.format, quality=80, save=False)
temp_name = os.path.split(self.image.name)[1]
self.image.save(temp_name, content=ContentFile(new_image.getvalue()), save=False)
super(Data, self).save(*args, **kwargs)
Here's the problem, I saved an image named tesla.jpg into the database, it compressed & resized it well, but it renamed it something like, tesla_CKBw0Kr_iWKC4Ry_ucPoh4b_BB2C8Ck_WrfWkPR_Tzig2T1_tdhst4b_3Bysn6k_i4ffhPR_yhig2T1.jpg
I'm worried about the new name because normally it should be tesla_CKBw0Kr.jpg or something smaller, so what's the problem in my code & how can we fix that?
Django mangles the image filename so that you don't run into filename collisions in the filesystem. Consider what if you had to save another image named tesla.jpg and don't want it to accidentally overwrite the first one.
You don't have to worry about that though. Django stores the real, original filename in the UploadeFile object.
UPDATE
Django will keep adding random characters to the filename if you upload more files with the same filename:
https://github.com/django/django/blob/master/django/core/files/storage.py#L60-L89
If you worry about hitting the filesystem's filename length limit, then set an appropriate max_length on the ImageField. The function will then keep truncating the file_name and generating new names within the filename length limit, until it finds a free name.

Implementing multiple person relationship

I've done a facebook like model, but I want the Personne to have more than one link with another Personne.
I have an intermediary table PersonneRelation with a custom save method. The idea is: when I add a relation to a person, I want to create another relation the other way. The problem is that if I try to save in the save method it's a recursive call. So my idea was to create a variable of the class and set it to True only when I want to avoid recursion.
Here's how I did:
class Personne(models.Model):
user = models.OneToOneField(User)
relations = models.ManyToManyField('self', through='PersonneRelation',
symmetrical=False)
class PersonneRelation(models.Model):
is_saving = False
# TAB_TYPES omitted for brevity
type_relation = models.CharField(max_length=1,
choices=[(a, b) for a, b in
list(TAB_TYPES.items())],
default=TYPE_FRIEND)
src = models.ForeignKey('Personne', related_name='src')
dst = models.ForeignKey('Personne', related_name='dst')
opposite = models.ForeignKey('PersonneRelation',
null=True, blank=True, default=None)
def save(self, *args, **kwargs):
if self.is_saving:
return super(PersonneRelation, self).save(args, kwargs)
old = None
if self.pk and self.opposite:
old = self.type_relation
retour = super(PersonneRelation, self).save(args, kwargs)
if old:
PersonneRelation.objects.filter(
src=self.dst, dst=self.src, opposite=self, type_relation=old
).update(type_relation=self.type_relation)
if self.opposite is None:
self.opposite = PersonneRelation(
src=self.dst, dst=self.src, opposite=self,
type_relation=self.type_relation, is_reverse=True)
self.opposite.save()
self.is_saving = True
self.save()
self.is_saving = False
return retour
My question is: is it safe to do so (using a class variable is_saving) (I dont know how Python deals with such variables)? If not, why? I feel like it's not ok, so what are the other possibilities to implement multiple many to many relationship that should behave like that?
Unfortunately, it's not safe, because it's not thread-safe. When two simultaneous Django threads will try to save your model, the behaviour can be unpredictable.
If you want to have more reliable locking, take a look, for example, at the Redis locking.
But to be honest, I'd try to implement it using plain reverse relations, maybe incapsulating the complexity into the ModelManager.
Here's how I modified it: I totally removed the save method and used the post_save message to check:
if it was created without opposite side, I create here with opposite side as the one created (and I can do it here without any problem!) then I update the one created with the "opposite"
if it wasn't created, this is an update, so just make sure the opposite side is changed as well.
I did this because I'll almost never have to change relationships between people, and when I'll create new ones there wont be any possible race conditions, because of the context where I will create new relationships
#receiver(post_save, sender=PersonneRelation)
def signal_receiver(sender, **kwargs):
created = kwargs['created']
obj = kwargs['instance']
if created and not obj.opposite:
opposite = PersonneRelation(
src=obj.dst, dst=obj.src, opposite=obj,
type_relation=obj.type_relation, is_reverse=True)
opposite.save()
obj.opposite = opposite
obj.save()
elif not created and obj.type_relation != obj.opposite.type_relation:
obj.opposite.type_relation = obj.type_relation
obj.opposite.save()
If I get the idea behind your code, then:
Django automatically makes relation available on both ends so you can get from src Personne to dst Personne via PersonneRelation and reverse dst -> src in your code. Therefore no need for additional opposite field in PersonneRelation.
If you need to have both symmetrical and asymmetrical realtions, i.e. src -> dst, but not dst -> src for particaular record, then I would suggest to add boolean field:
class PersonneRelation(models.Model):
symmetrical = models.BooleanField(default=False)
this way you can check if symmetrical is True when accessing relation in your code to identify if it's scr -> dst only or both src -> dst and dst -> src. In facebook terms: if symmetrical is False you get src is subscriber of dst, if it's True you get mutual friendship between src and dst. You might want to define custom manager to incapsulate this behavior, though it's more advanced topic.
If you need to check if the model instance is being saved or updated, there's no need in is_saving boolean field. Since you're using automatic primary key field, you can just check if pk on model instance is None. In Django before the model instance is first time saved to DB ('created') pk is None, when the instance is 'updated' (it has been read from DB before and is being saved now with some field values changed) it's pk is set to pk value from DB. This is the way Django ORM decides if it should update or create new record.
In general when redefining Save method on a model, or when using signals like pre_save/post_save take into consideration, that those functions you define on them might not be called by Django in some circumstances, i.e. when the model is updated in bulk. See Django docs for more info.

django - How to copy actual image file from one model to another?

I want to copy images from one model to another within the project. Suppose these are my models:
class BackgroundImage(models.Model):
user = models.ForeignKey(User)
image = models.ImageField(upload_to=get_upload_file_name)
caption = models.CharField(max_length=200)
pub_date = models.DateTimeField(default=datetime.now)
class ProfilePicture(models.Model):
user = models.ForeignKey(User)
image = models.ImageField(upload_to=get_upload_file_name)
caption = models.CharField(max_length=200)
pub_date = models.DateTimeField(default=datetime.now)
#classmethod
def create_from_bg(cls, bg_img):
img = cls(user=bg_img.user, image=bg_img.image, caption=bg_img.caption+'_copy', pub_date=bg_img.pub_date)
img.save()
return img
For now, I can do these:
To get the user
>>>m = User.objects.get(username='m')
To get the user's profile picture set
>>>m_pro_set = m.profilepicture_set.all()
>>>m_pro_set
[<ProfilePicture: pro_mik>]
Get an image object from Background image of the user
>>>m_back_1 = m.backgroundimage_set.get(id=2)
>>>m_back_1
<BackgroundImage: bg_mik>
And then:
>>>profile_pic = ProfilePicture.create_from_bg(m_back_1)
Now when I check it, it does create a new instance.
>>>m_pro_set
[<ProfilePicture: pro_mik>,<ProfilePicture: bg_mik>]
But, if I check on the path, and even on the media folder, its the same image and not an actual copy of the image file.
>>>profile_pic.image
<ImageFileField: uploaded_files/1389904144_ken.jpg>
>>>m_back_1.image
<ImageFileField: uploaded_files/1389904144_ken.jpg>
How do I go about, to actually copy the original image file within the models? Any help will be much appreciated! Thank you.
so I know this question is pretty old, but hopefully this answer help someone...
My approach for doing this, uploading the photo to the proper path for the suggested model, is:
from django.core.files.base import ContentFile
picture_copy = ContentFile(original_instance.image.read())
new_picture_name = original_instance.image.name.split("/")[-1]
new_instance.image.save(new_picture_name, picture_copy)
Please check that in my case, the new name is just the same file name, but to be updated in the new model image field's path. In your case, depending on what you have inside "get_upload_file_name" it could leads to the same path again (since is used in both classes). Also you can create a new, random name.
Hope this helps someone =)
Best & Short solution is that
existing_instance = YourModel.objects.get(pk=1)
new_instance.image = existing_instance.image
It's work fine for me.

Can't set ImageField url attribute

I want to change my ImageField's attribute, however I'm constantly getting the Can't set attribute error.
My model is
class Society(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
summary = models.TextField(blank=True,null=True)
members = models.ManyToManyField(User,null=True,blank=True)
gallery = models.ForeignKey(Gallery,null=True,blank=True)
avatar = models.ImageField(upload_to=get_society_path)
def save(self,*args,**kwargs):
super(Society, self).save(*args,**kwargs)
fix_avatar_path(self)
def clean(self):
if self.id:
self.avatar.path = get_society_path(self,self.avatar.path)
save_thumb(self.avatar.path)
And my helper functions are :
def get_society_path(instance,filename):
seperator_val = instance.id
if seperator_val is None:
seperator_val = get_time()
return '%s/society_%s/%s' % (settings.UPLOAD_ROOT,seperator_val,time_to_name(filename))
def fix_avatar_path(instance):
org_society_path = get_society_path(instance,instance.avatar.name)
make_upload_dir(org_society_path)
move(instance.avatar.path,org_society_path)
os.rmdir(os.path.dirname(instance.avatar.path))
instance.clean()
The problem is :
I want to save my society directories as society_society_id. But normally, I can't assign any id before the model is saved. So i'm creating a tmp file whose name is a time value.Then to reach societies folder, I want to rename this file. So, my fix_avatar simply moves the tmp file's content to society_(society_id) folder after the society is saved. So far so good everything works well. However, my society's ImageField still holds the previously created folder. In order to change it's value, I found that i can use clean method.(from this SO question) But still i'm getting the same result, the path doesn't change, and gives the "can't set attribute" response.
Any idea ??
Not sure, if this was ever changed in Django since this question was asked. A ticket about this not being possible still exists: https://code.djangoproject.com/ticket/15590
However, you actually can change the path by doing it like this:
self.avatar = 'uploads/example/path/'
What also does the job:
self.avatar.name = 'uploads/example/path/'
It has worked for us in several occasions.
The problem is here:
self.avatar.path = get_society_path(self,self.avatar.path)
You cannot change the value of the path attribute in FileField/ImageField instances, it's readonly. There is a proposal to change this in Django 1.4