In summary, what I'm trying to accomplish here is:
To perform transformation over uploaded files' names for organization and security's sake
Not to keep old files in storage forever
Let's say that you have a Company model which has a logo. Something like
class Company(models.Model):
name = ...
logo = models.FileField(blank=True, null=True)
Now, since I'm a bit paranoid and I don't really like the fact that uploaded files get the name given by the (potentially evil) user, I add a upload_to parameter that points to a function that's something like
def logo_getfilename(instance, filename):
extension = ... # get just the original extension from the file
return 'logos/' + str(uuid.uuid4()) + extension
Ok, so now only the view is missing!
def company_edit(request, ...):
company = ... # get the company and stuff
if request.method == 'POST':
form = CompanyAdminForm(request.POST, request.FILES, instance=company)
last_file_path = None
if not company.logo is None:
last_file_path = company.logo.path
# ^^ after calling is_valid(), file_path gets changed to
# the would-be-default-behavior
if form.is_valid():
# first we write the new file
form.save()
# now we remove the old one
os.unlink(last_file_path)
Although this is currently working, I'm not really comfortable with it because
I'm using os.unlink() instead of FieldFile.delete() which seems wrong
I'm assuming a local filesystem storage
I'm still not doing anything against naming collisions (they may still happen)
I'm disregarding multiple chunks and assuming that form.save() will deal with everything
I'm not considering transactional behavior (previous file should be deleted only after the .save() model changes are commited to the database
I feel there are some problems there I don't even know about
So to acheive these simple (and not as uncommon as that) goals, what would be your recommendations?
Instead of making a random username, why not use the primary key for the Company? When they upload a new file, just have it overwrite the existing one (which I think it will do automatically as long as you change the file name before you save). This should remove your need to do os.unlink and (maybe) stop your transactional worries.
Related
How to make only one field of these two fields?
is it possible?
class MyModel(models.Model):
video_file = models.FileField(blank=True)
video = models.URLField(blank=True)
def clean(self, *args, **kwargs):
if not self.video_file and not self.video: # This will check for None or Empty
raise ValidationError({'video_file': 'Even one of field1 or field2 should have a value.'})
elif self.video_file and self.video: # This will check for None or Empty
raise ValidationError({'video_file': 'Even one of field1 or field2 should have a value.'})
if self.video == '':
self.video = self.video_file.url
super(MyModel, self).save(*args, **kwargs)```
**UPDATED**
I think this is the solution, my bad.
Having only one option
The easiest solution might be to not accept 2 different types, and only support either Image upload or Image URL. I'd suggest Image upload only if you're going to implement this solution.
However, if having those 2 options is a requirement you can take a look at the solutions I've listed below.
Checking at a controller level (Simple solution)
One solution is to check if both fields are populated at the controller level, or View in django jargon. If both are populated you can throw some error and handle it from there.
Changing the model and handling at service level (Recommended)
The above solution might work, but that wouldn't be the ideal solution for the long run.
I'd recommend you to change your model to only have a FileField, then in service layer you can directly upload if user uploads a file, however if user passes a URL you can download the image and save it.
You can also make the DB field a UrlField, and if user uploads a file, you can upload it to some external storage bucket like s3 or cloudinary and save the URL in your database.
As for the constraint, you can apply the constraint as mentioned above in solution 2 of adding the constraint in controller or some other way using django magic.
I HAVE scoured the web and if there is a solution I have not found it. Perhaps the answer is right in front of me but I don't see it and I'm really baffled at this issue.
Problem: I have a model:
class Campaign(models.Model):
emailaddress = models.CharField(max_length=100, default='', verbose_name='ADF Email', blank=True)
xlsfile = models.FileField(upload_to='xlsfiles/', blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
dealerimage = models.ForeignKey('vipapp.savedImage', blank=True, null=True, related_name='campaign')
I do not override the admin add/change form currently. Perhaps I should, we'll see.
When I submit a new campaign an xlsfile and dealerimage are uploaded and should be required. I use if(self.xlsfile): in the overridden save() on the campaign model to check for the optional xlsfile and if it exists I have a considerable amount of code that parses the xlsfile, puts the records into another model called weblist and also sends the user a modified excel file (I am using both xlsfile and excel file naming on purpose in this post).
The problem is this: When the user edits a campaign using the default admin change form the excel file is listed in the xlsfile field. On submit, the system appears to try again to upload the excel file and that file may no longer actually exist on the system from which the user is editing. On edit, the user may not want to upload a new excel file as this would overwrite the other model (weblist).
Question: How would I clear this field in the change form yet not delete it from the object when the user submits? I cannot use exclude because the user MAY want to upload a new excel file.
Question: What is self.xlsfile in the save() on the campaign model? I mean, how do I tell the difference between what is in the database already (maybe obj.xlsfile) and what is coming in from the change form POST? Is new data in the post self.xlsfile or is that what is in the object already? I know I should be able to determine these things on my own but I've not been able to reliable tell if my test is correct so that it gives me the correct answer.
How do I effectively clear the field when the form loads? The user MAY want to replace the xls and therefore delete the weblist records but this is not typical. Replacing the weblist instances in the weblist model is dangerous because these contain generated codes that have already been sent to customers so this rarely happens, so if there is no new excel file the weblist instances should not be touched. If the user does upload a new excel file the current weblist instances should be deleted and re-written however.
exclude on the edit form is not an option because again, xlsfile may sometimes need to be updated though this is rare. I don't want to delete anything, but I don't want the form to show the xls file or make any changes unless the user explicitly uploads a new xlsfile. I may have repeated myself here but I just want to be clear.
I suspect the answer here will affect the dealerimage field as I will probably understand this better in general. Any explanatory info here would be awesome. Also, providing me links to Working with Forms or The Forms API or Form and field Validation pages will likely be unhelpful as I have read these documents. Pointing to an exact answer that I may have missed in those pages however may provide an Ah-ha moment though :)
I figured this out...
I just overrode get_form so that xlsfile field would always be set to blank on load of my CampaignEditForm:
def get_form(self, request, obj=None, **kwargs):
if obj:
obj.xlsfile = ''
kwargs['form'] = CampaignEditForm
else:
kwargs['form'] = CampaignAddForm
return super(campaignAdmin, self).get_form(request, obj, **kwargs)
Then in the save() of my campaign model I just explicitly set the fields I wanted to save, leaving out xlsfile if it was blank so any value in the DB never got deleted unless it was replaced:
if self.xlsfile:
super(Campaign, self).save(*args,**kwargs)
else:
super(Campaign, self).save(update_fields=["dealername", "campaignname", "dealeraddress", "dealercity",
"dealerstate", "dealerzipcode", "dealerphone", "dealeremail1",
"dealeremail2", "dealeremail3", "dealeremail4", "dealeremail5",
"adf", "adfemail", "created", "dealerimage"])
I have a FilePathField in a form that displays directory contents as
expected. However if I add or remove a file to/from the directory the
change is not updated in the form.
Here is the from:
from django import forms
class MyForm(forms.Form):
the_file = forms.FilePathField(path='c:/temp')
and the corresponding view:
from django.shortcuts import render_to_response, get_object_or_404
from forms import MyForm
def test(request):
form = MyForm()
return render_to_response('test.html', {'form' : form})
I ran into this today, after noticing that new folders weren't being reflected in the select menu.
The FilePathField is initialized once, when django starts up (when the Form class is parsed by python). The contents of the directory are then stored in the field itself, and then persist for the lifetime of the program.
This behavior is unexpected (at least by me) and not explicitly documented.
The discussion below implies that this only happens when a FilePathField is used in an explicit Form class, rather than in a dynamically generated ModelForm.
See: https://groups.google.com/forum/#!searchin/django-developers/FilePathField$20refresh/django-developers/mHK-9N75swM/3k13dC5HVQoJ
And the relevant bug: https://code.djangoproject.com/ticket/16429
Neither has received much attention on the forums, so I suspect this falls into edge-case territory, probably because the fixes for it are likely to be messy.
Update I've implemented a silly workaround. Whenever you need a new form, the __init__ method is called. In that method, initialize a new FilePathField with the same arguments are the original one. The new one will have a fresh copy of the directory entries in it, and replace the stale choices with the fresh copy.
To keep from abusing the file system, keep a copy of these choices around in the cache for 5 seconds.
from django import forms
from django.core.cache import cache
class MyAwesomeForm(forms.Form):
_filepath_kw = dict(path='/some/dir',
label="Path to source images.",
recursive=True, allow_files=False,
allow_folders=True)
my_field = forms.FilePathField(**_filepath_kw)
def __init__(self, **kwargs):
key = 'filepath-cache-key'
choices = cache.get(key)
if not choices:
field = forms.FilePathField(**self._filepath_kw)
choices = field.choices
cache.set(key, choices, 5)
super().__init__(**kwargs)
self.base_fields['source'].choices = choices
If your directory is huge, storing zillions of file records in the cache is likely quite slow and probably a bad idea.
This will cause validation issues if the file system changes right after the POST, but, that's probably a good thing.
I'm pushing this change to an application this afternoon, so if I find terrible unforeseen consequences I'll update this answer.
If you dont want to use a cache but you were happy with that each time you call this form it reads the actual files or folders, then you could use this solution:
class ApproveUpdateForm(forms.Form):
#to ensure that the file list is updated each time ApproveUpdateForm is called
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fields['app'] = forms.FilePathField(path=settings.EXAMPLE_EXCH_DIR, match='.*\.tar.gz',
label=_("Application"))
self.fields['app'].widget.attrs['class'] = 'form-control'
I Am trying to give an uploaded image a nicer path, using this code (in models.py):
def get_image_path_photos(instance, filename):
return os.path.join('photos', str(instance.someproperty), filename)
and the model
class Photo(models.Model):
someproperty = models.CharField(max_length=17, blank=False, null=False, default="something")
photo = models.ImageField(upload_to=get_image_path_photos, blank=True, null=True)
When I save this after a new insert, it saves it in the path /photos/something/ (it keeps using the default value).
When I edit it, add a photo and save it, it will save it to the correct path.
So it must have something to do that while saving the new object, it doesn't exist yet.
I tried the same with instance.id and this keeps being None as well (I read using auto increment on the id solves this, but this sounds as using the default value as well, and using the default pk/id is auto increment).
I found some simular questions, but none with the answer that solves my problem.
I thought of going to use the pre_save signal.... but somehow my guts says this isn't the right way.
The solution of my problem I found out myselve, please see my answer... A good lesson, don't use slugname definitions the same as the field name.....
Sorry about this. The problem is a bit more complicated. I use the field someproperty also in the url as a slug on the posts....
I just found out something I didn't expected.
i did my post (using django rest framework) from the url using the default value in the url... but I filled in the field with something else.
than, because I define the slugname the same as the fieldname, it overwrites anything you fill in in the field with the value from the url....
This isn't exactly what I meant to be done, but makes sence.
Probably the solution is to call the slug name not the same as the field name......
I keep this question and answer anyway, because for me it was quite a puzzle..... (might be of help to somebody)
as an addition to the answer of jpic:
I used the urls in django rest framwwork, lets say; http:\someurl\api\photos\\
and post there the photo.
posting the photo avatar_big.png using someproperty=bar:
saved the photo in photos\something\ when using the url http\someurl\api\photos\something
and saved the photo in photos\bar\ when using the url http:\someurl\api\photos\bar
the problem is (i guess, still have to check this) that the slug name I use for the url is the same as the fieldname.
this is the code I use in views.py (class based view I use in the django-rest-framework):
class PhotoBySomePropertyListOrCreateModelView(ListOrCreateModelView):
permissions = (IsAuthenticated, )
form = PhotoForm
def get_queryset(self):
someproperty=self.kwargs['someproperty']
return Photo.objects.filter(someproperty=someproperty)
and in urls.py:
url(r'^api/photos/(?P<someproperty>[\w:]+)/$', PhotoBySomePropertyListOrCreateModelView.as_view(resource=PhotoResource)),
here you see the problem, it doesn't listen to the field in 'someproperty', but to the value in the url ....
changing it in
url(r'^api/photos/(?P[\w:]+)/$', PhotoBySomePropertyListOrCreateModelView.as_view(resource=PhotoResource)),
should do the trick.... also adjust the view of course
I have a userprofile of the form
class profile():
#the next line is just an abstract
profile_images='volumes/media/root/userprofile/profile_images/'
image=models.ImageField(upload_to=profile_images)
in the directory "profile_images" there are the last 5 files the user uploaded as profile images, ie:
image_1
image_2
image_3
image_4
image_5
lets say the current profile.image is image_1. now i want to allow the user to select one of the previous images. the function i wrote to change the image to the one i received from the form looks like that:
def change_profile_image(userprofile,path_to_new_image):
f = open(path_to_new_image, 'r')
userprofile.image = ImageFile(f)
userprofile.save()
as an example the user selects image_3, and after execution of that code the forementioned directory looks like that:
image_1
image_2
image_3
image_4
image_5
volumes/media/root/userprofile/profile_images/image_3
which, of course, is not what i wanted. what i want is to just change the file associated with the ImageField of my profile instance, without Django copying any files.
any ideas how to solve that?
ok, actually it's as easy as
userprofile.image=path_to_new_image
no need to worry with opening files, deleting and rewriting them.
Theoretically, you could overwrite userprofile.image.path, but it’s not too obvious how to do that.
Here is some more information.
Programmatically saving image to Django ImageField
Django: How to replace/overwrite/update/change a file of FileField?
How can I replace/override an uploaded file?