Allow SVG files to be uploaded to ImageField via Django admin - python

I'm switching to SVG images to represent categories on my e-commerce platform. I was using models.ImageField in the Category model to store the images before, but the forms.ImageField validation is not capable of handling a vector-based image (and therefore rejects it).
I don't require thorough validation against harmful files, since all uploads will be done via the Django Admin. It looks like I'll have to switch to a models.FileField in my model, but I do want warnings against uploading invalid images.
Nick Khlestov wrote a SVGAndImageFormField (find source within the article, I don't have enough reputation to post more links) over django-rest-framework's ImageField. How do I use this solution over Django's ImageField (and not the DRF one)?

I have never used SVGAndImageFormField so I cannot really comment on that. Personally I would have opted for a simple application of FileField, but that clearly depends on the project requirements. I will expand on that below:
As mentioned in the comment, the basic difference between an ImageField and a FileField is that the first checks if a file is an image using Pillow:
Inherits all attributes and methods from FileField, but also validates that the uploaded object is a valid image.
Reference: Django docs, Django source code
It also offers a couple of attributes possibly irrelevant to the SVG case (height, width).
Therefore, the model field could be:
svg = models.FileField(upload_to=..., validators=[validate_svg])
You can use a function like is_svg as provided in the relevant question:
How can I say a file is SVG without using a magic number?
Then a function to validate SVG:
def validate_svg(file, valid):
if not is_svg(file):
raise ValidationError("File not svg")

It turns out that SVGAndImageFormField has no dependencies on DRF's ImageField, it only adds to the validation done by django.forms.ImageField.
So to accept SVGs in the Django Admin I changed the model's ImageField to a FileField and specified an override as follows:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = []
field_classes = {
'image_field': SVGAndImageFormField,
}
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
admin.site.register(MyModel, MyModelAdmin)
It now accepts all previous image formats along with SVG.
EDIT: Just found out that this works even if you don't switch from models.ImageField to models.FileField. The height and width attributes of models.ImageField will still work for raster image types, and will be set to None for SVG.

Here is a solution that works as a simple model field, that you can put instead of models.ImageField:
class Icon(models.Model):
image_file = SVGAndImageField()
You need to define following classes and functions somewhere in your code:
from django.db import models
class SVGAndImageField(models.ImageField):
def formfield(self, **kwargs):
defaults = {'form_class': SVGAndImageFieldForm}
defaults.update(kwargs)
return super().formfield(**defaults)
And here is how SVGAndImageFieldForm looks like:
from django import forms
from django.core.exceptions import ValidationError
class SVGAndImageFieldForm(forms.ImageField):
def to_python(self, data):
try:
f = super().to_python(data)
except ValidationError:
return validate_svg(data)
return f
Function validate_svg I took from other solutions:
import xml.etree.cElementTree as et
def validate_svg(f):
# Find "start" word in file and get "tag" from there
f.seek(0)
tag = None
try:
for event, el in et.iterparse(f, ('start',)):
tag = el.tag
break
except et.ParseError:
pass
# Check that this "tag" is correct
if tag != '{http://www.w3.org/2000/svg}svg':
raise ValidationError('Uploaded file is not an image or SVG file.')
# Do not forget to "reset" file
f.seek(0)
return f
Also if you want to use SVG files only model field - you can do it more simple.
Just create class, inherited from models.FileField, and in __init__ method you can add validate_svg function to kwargs['validators'].
Or just add this validator to models.FileField and be happy :)

As stated in the comments, validation for SVGAndImageFormField will fail because extensions are checked using django.core.validators.validate_image_file_extension, which is the default validator for an ImageField.
A workaround for this would be creating a custom validator adding "svg" to the accepted extensions.
Edited: Thanks #Ilya Semenov for your comment
from django.core.validators import (
get_available_image_extensions,
FileExtensionValidator,
)
def validate_image_and_svg_file_extension(value):
allowed_extensions = get_available_image_extensions() + ["svg"]
return FileExtensionValidator(allowed_extensions=allowed_extensions)(value)
Then, override the default_validators attribute in the SvgAndImageFormField:
class SVGAndImageFormField(DjangoImageField):
default_validators = [validate_image_and_svg_file_extension]
# ...

from django.forms import ModelForm, FileField
class TemplatesModelForm(ModelForm):
class Meta:
model = Templates
exclude = []
field_classes = {
'image': FileField,
}
#admin.register(Templates)
class TemplatesAdmin(admin.ModelAdmin):
form = TemplatesModelForm
its work

Related

Convert (and validate) file added in django admin

I'm working on a Django project that utilizes customized greetings (like in voicemail). The whole functionality is implemented, i have created a custom model:
class Greeting(models.Model):
audio_file = models.FileField(upload_to='greetings/')
description = models.CharField(max_length=128)
uploaded_at = models.DateTimeField(auto_now_add=True)
The next thing that i wanted to do is to make sure that the uploaded file has all the expected properties (is a WAV file, has one channel, has low bitrate etc). But i don't even know where to start. These files will be only added via django admin. In regular FormView i would utilize server-sided validation in View, and only then add it to model. How to do it in django admin?
To summarize what i expect my app to do:
1) Add file to a model in django admin
2) Server checks file properties, and if requirements are not met, tries to convert it to proper format 3) If the file is in proper format, only then it saves the object.
You need to register a ModelAdmin with a custom form.
ModelAdmin has a form property which is by default set to forms.ModelForm class, you can replace that by assigining that property to your Admin class.
# app_dir/admin.py
from django.contrib import admin
from .forms import GreetingAdminForm
from .models import Greeting
#admin.register(models.Greeting)
class GreetingAdmin(admin.ModelAdmin):
form = GreetingAdminForm
readonly_fields = ['uploaded_at']
Than you need to define your GreetingAdminForm in forms.py. with custom validation Logic.
The way I would do it is add a ModelForm with overridden audo_file field with added validators. You can check the django documentation for writing your validation logic here
Probaly you want to use file extension validation, and add a clean_{fieldname} method on the form.
The clean_{fieldname} method does not take any arguments but the return value of this method must replace the existing value in cleaned_data. You will need an external library that suits your needs, accepts audio formats that you intend to allow, and outputs processed file in desired format. Docs on cleaning specific attribiutes are here
# app_dir/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .validators import validate_file_extension
from .models import Greeting
class GreetingAdminForm(forms.ModelForm):
audio_file = forms.FileField(validators=[validate_file_extension])
def clean_audio_file(self):
data = self.cleaned_data
processed_audio_file = None
# audio file processing logic goes here,
if not processed_audio_file:
raise ValidationError('error message')
data['audio_file'] = processed_audio_file
return data
class Meta:
model = Greeting
fields = [
'audio_file',
'description'
]
# app_dir/validators.py
def validate_file_extension(value):
# validate file extension logic here,
you can find a example of file extension validation
here
Another angle to approach this could be also
- writing a custom form field which subclasses the FileField,you can find documentation on writing your own field here, this class should override w methods validate() - which handles validation logic, and to python where you would prepare output to be available in your python code

How do I override django admin's default file upload behavior?

I need to change default file upload behavior in django and the documentation on the django site is rather confusing.
I have a model with a field as follows:
class document (models.Model):
name = models.CharField(max_length=200)
file = models.FileField(null=True, upload_to='uploads/')
I need to create a .json file that will contain meta data when a file is uploaded. For example if I upload a file mydocument.docx I need to create mydocument.json file within the uploads/ folder and add meta information about the document.
From what I can decipher from the documentation I need to create a file upload handler as a subclass of django.core.files.uploadhandler.FileUploadHandler. It also goes on to say I can define this anywhere I want.
My questions: Where is the best place to define my subclass? Also from the documentation found here https://docs.djangoproject.com/en/1.8/ref/files/uploads/#writing-custom-upload-handlers looks like the subclass would look like the following:
class FileUploadHandler(object):
def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None):
# do the acctual writing to disk
def file_complete(self, file_size):
# some logic to create json file
Does anyone have a working example of a upload handler class that works for django version 1.8?
One option could be to do the .json file generation on the (model) form used to initially upload the file. Override the save() method of the ModelForm to generate the file immediately after the model has been saved.
class DocumentForm(forms.ModelForm):
class Meta(object):
model = Document
fields = 'name', 'file'
def save(self, commit=True):
saved_document = super().save(commit)
with open(saved_document.file.path + '.json', mode='w') as fh:
fh.write(json.dumps({
"size": saved_document.file.size,
"uploaded": timezone.now().isoformat()
}))
return saved_document
I've tested this locally but YMMV if you are using custom storages for working with things like S3.

Django Admin - how to prevent deletion of some of the inlines

I have 2 models - for example, Book and Page.
Page has a foreign key to Book.
Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).
In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).
My problem - how can I achieve the behavior that a page that was read won't be deleted?
I'm using Django 1.4 and I tried several options:
Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.
Are there any other good options without overriding the html code?
Thanks,
Li
The solution is as follows (no HTML code is required):
In admin file, define the following:
from django.forms.models import BaseInlineFormSet
class PageFormSet(BaseInlineFormSet):
def clean(self):
super(PageFormSet, self).clean()
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
curr_instance = form.instance
was_read = curr_instance.was_read
if (data.get('DELETE') and was_read):
raise ValidationError('Error')
class PageInline(admin.TabularInline):
model = Page
formset = PageFormSet
You could disable the delete checkbox UI-wise by creating your own custom
formset for the inline model, and set can_delete to False there. For
example:
from django.forms import models
from django.contrib import admin
class MyInline(models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInline, self).__init__(*args, **kwargs)
self.can_delete = False
class InlineOptions(admin.StackedInline):
model = InlineModel
formset = MyInline
class MainOptions(admin.ModelAdmin):
model = MainModel
inlines = [InlineOptions]
Another technique is to disable the DELETE checkbox.
This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
if some_criteria_to_prevent_deletion:
form.fields['DELETE'].disabled = True
This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.
In your inline, you can add the flag can_delete=False
EG:
class MyInline(admin.TabularInline):
model = models.mymodel
can_delete = False
I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method.
This works not just on admin, but on regular inlines too.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
#property
def deleted_forms(self):
deleted_forms = super(MyInlineFormSet, self).deleted_forms
for i, form in enumerate(deleted_forms):
# Use form.instance to access object instance if needed
if some_criteria_to_prevent_deletion:
deleted_forms.pop(i)
return deleted_forms

Django prepopulated_fields like method

Please forgive my naiveté with Django.
I want to create my own method which works much like prepopulated_fields for my custom admin page. Basically, when you put the url of an image in one field, I'd like to populate another field with the name of the image, height and width via javascript.
What would be the best approach? Just override the change_form.html and include my own JS lib? Write a custom widget?
Using Javascript would be a probable move, if for some reason you want to show the attributes of the image.
See Javascript - Get Image height for an example of doing this.
If there's no need to show it at the form level but to simply populate the usually prefer to do this at the model level, such as
from PIL import Image
import StringIO
import urllib2
class MyModel(models.Model):
# ... fields 1,2,3 etc, and assuming the url field is called image_url
def pre_save():
# obtain attributes of image from url field
# save it to various fields
img = urllib2.urlopen(self.image_url).read()
im = Image.open(StringIO.StringIO(img))
self.image_width, self.image_height = im.size
def save(self, *args, **kwargs):
self.pre_save()
super(MyModel, self).save(*args, **kwargs)
Good luck!

How to delete old image when update ImageField?

I'm using Django to create a stock photo site, I have an ImageField in my model, the problem is that when the user updates the image field, the original image file isn't deleted from the hard disk.
How can I delete the old images after an update?
Use django-cleanup
pip install django-cleanup
settings.py
INSTALLED_APPS = (
...
'django_cleanup.apps.CleanupConfig', # should be placed after your apps
)
You'll have to delete the old image manually.
The absolute path to the image is stored in your_image_field.path. So you'd do something like:
os.remove(your_image_field.path)
But, as a convenience, you can use the associated FieldFile object, which gives easy access to the underlying file, as well as providing a few convenience methods. See http://docs.djangoproject.com/en/dev/ref/models/fields/#filefield-and-fieldfile
Use this custom save method in your model:
def save(self, *args, **kwargs):
try:
this = MyModelName.objects.get(id=self.id)
if this.MyImageFieldName != self.MyImageFieldName:
this.MyImageFieldName.delete()
except: pass
super(MyModelName, self).save(*args, **kwargs)
It works for me on my site. This problem was bothering me as well and I didn't want to make a cleanup script instead over good bookkeeping in the first place. Let me know if there are any problems with it.
Before updating the model instance, you can use the delete method of FileField object. For example, if the FileField or ImageField is named as photo and your model instance is profile, then the following will remove the file from disk
profile.photo.delete(False)
For more clarification, here is the django doc
https://docs.djangoproject.com/en/1.11/ref/models/fields/#django.db.models.fields.files.FieldFile.delete
You can define pre_save reciever in models:
#receiver(models.signals.pre_save, sender=UserAccount)
def delete_file_on_change_extension(sender, instance, **kwargs):
if instance.pk:
try:
old_avatar = UserAccount.objects.get(pk=instance.pk).avatar
except UserAccount.DoesNotExist:
return
else:
new_avatar = instance.avatar
if old_avatar and old_avatar.url != new_avatar.url:
old_avatar.delete(save=False)
My avatrs has unique url for each person like "avatars/ceb47779-8833-4719-8711-6f4e5cabb2b2.png". If user upload new image with different extension like jpg, delete_file_on_change_extension reciever remove old image, before save new with url "avatars/ceb47779-8833-4719-8711-6f4e5cabb2b2.jpg" (in this case). If user uploads new image with same extension django overwrite old image on storage (disk), because images paths are the same.
This works fine with AWS S3 django-storage.
Here is an app that deletes orphan files by default: django-smartfields.
It will remove files whenever:
field value was replaced with a new one (either uploaded or set manually)
field is cleared through the form (in case that field is not required, of course)
the model instance itself containing the field is deleted.
It is possible to turn that cleanup feature off using an argument: ImageField(keep_orphans=True) on per field basis, or globally in settings SMARTFIELDS_KEEP_ORPHANS = True.
from django.db import models
from smartfields import fields
class MyModel(models.Model):
image = fields.ImageField()
document = fields.FileField()
try this, it will work even if old file is deleted
def logo_file(instance, filename):
try:
this = business.objects.get(id=instance.id)
if this.logo is not None:
path = "%s" % (this.logo)
os.remove(path)
finally:
pass..
code will work even without "try .. finally" but it will generate problem if file was accidently deleted.
changed: move model matching inside "try" so it will not throw any error at user signup
Let me know if there are any problems.
Completing Chris Lawlor's answer, tried this and works.
from YOURAPP.settings import BASE_DIR
try:
os.remove(BASE_DIR + user.userprofile.avatarURL)
except Exception as e:
pass
The URL has a pattern of /media/mypicture.jpg
What I did is saving the path to the old image and if form is valid I would delete the old one.
if request.method == 'POST':
old_image = ""
if request.user.profile.profile_picture:
old_image = request.user.profile.profile_picture.path
form = UpdateProfileForm(request.POST,request.FILES,instance = profile)
if form.is_valid():
if os.path.exists(old_image):
os.remove(old_image)
form.save()
It is a little messy , but you do not install third parties or anythin

Categories

Resources