Django rename uploaded file: append specific string at the end - django

I'm restricting the upload button to allow only csv files.
I need help please to append _hello at the end of each file uploaded by the user, but before the extension. (e.g. user_file_name.csv becomes automatically user_file_name_hello.csv)
Optional: I'd like the original file to be first renamed automatically, then saved to my uploads directory.
models.py
from django.db import models
# validation method to check if file is csv
from django.core.exceptions import ValidationError
def validate_file_extension(value):
if not value.name.endswith('.csv'):
raise ValidationError(u'Only CSV files allowed.')
# Create your models here.
class user_file(models.Model):
user_file_csv = models.FileField(upload_to='documents/user_files/', validators=[validate_file_extension])
forms.py
from django import forms
from .models import user_file
from django.forms import FileInput
class user_file_form(forms.ModelForm):
class Meta:
model = user_file
widgets = {'user_file_csv': FileInput(attrs={'accept': 'text/csv'})}
fields = ('user_file_csv',)
Thank you!

Maybe you need something like this:
class FileUploadUtil:
#staticmethod
def my_files_path(instance, filename):
name, file_extention = os.path.splitext(filename)
name = 'prefix-{}-{}-sufix.{}'.format(name, instance.id, file_extention)
return "my_files/{}".format(name)
class MyModel(models.Model):
# Other fields
# ...
my_file = models.FileField(max_length=300, upload_to=FileUploadUtil.my_files_path)

Optional: I'd like the original file to be first renamed automatically, then saved to my uploads directory.
You can override save() method. Check here
Django document
Maybe You need decorator.
from pathlib import Path
def rename_helper(path: str, append_text: str):
stem, suffix = Path(path).stem, Path(path).suffix
return f"{stem}{append_text}{suffix}"
def rename_previous_image(func):
""" return wrapper object """
def wrapper(*args, **kwargs):
self = args[0]
model = type(self)
previous_obj = model.objects.filter(pk=self.pk)
if previous_obj.exists():
old_name_with_path = Path(str(previous_obj[0].user_file_csv))
Path.rename(old_name_with_path , rename_helper(path=old_name_with_path , append_text="_hello"))
return func(*args, **kwargs)
return wrapper
And, You can decorate your model save() method.
class MyModel(models.Model):
# Other fields
# ...
my_file = models.FileField(max_length=300, upload_to=FileUploadUtil.my_files_path)
#rename_previous_image
def save(self, **kwargs):
super(user_file, self).save(**kwargs) # You must add This row.
besides,
recommend rename your user_file class
like UserFile
Check This PEP 8
Have a good day.

Related

How in Django to override the "read" method only in/for the admin?

I know that they don't do this, but for one of my pet-projects I want a strange thing: store jinja-templates in the database (and be able to edit them through the admin panel).
There is something like this model (in models.py):
class TbTemplate(models.Model):
szFileName = models.CharField(
primary_key=True,
db_index=True,
unique=True,
verbose_name="Path/Name"
)
szJinjaCode = models.TextField(
verbose_name='Template',
help_text='Template Code (jinja2)'
)
szDescription = models.CharField(
max_length=100,
verbose_name='Description'
)
def __unicode__(self):
return f"{self.szFileName} ({self.szDescription})"
def __str__(self):
return self.__unicode__()
class Meta:
verbose_name = '[…Template]'
verbose_name_plural = '[…Templates]'
Next, in view.py you can do something like this:
# -*- coding: utf-8 -*-
from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string
from web.models import TbTemplate
def something(request: HttpRequest, template: str) -> HttpResponse:
"""
:param request: http-in
:param template: Template name
:return response: http-out
"""
to_template = {}
# ...
# ... do smth
# ...
tmpl = TbTemplate.objects.get(pk=template)
html = render_to_string(tmpl.szJinjaCode, to_template)
return HttpResponse(html)
And everything works. Templates to available for editing through the admin panel (of course, you need to hang a "mirror"-like widget for syntax highlighting, etc.)...
But I want to use in jinja templates like: {% include "something_template.jinja2" %} ... And for this it is necessary that the templates are not only in the database, but also stored as files in the templates-folder.
In addition, templates are easier to create and edit in IDEs, and access to templates through the admin panel only for cosmetic changes.
And then I need to somehow intercept the "read" method in/for the admin panel. So that if a template in the TbTemplate table is opened for editing in the admin panel, then for the szJinjaCode it was read not from the database, but from the corresponding szFileName-file.
How to do this?
It is done in two steps:
Firstly,
in models.py we will override the save() method for the TbTemplate model. At the same time, we can override delete() method, so that it do not delete anything (or vice versa, it deletes not only the entry in the database, but also the corresponding teplate-file... or deletes the entry in the database, and renames the corresponding file...). We get this model:
# -*- coding: utf-8 -*-
from django.db import models
from project.settings import *
import os
class TbTemplate(models.Model):
szFileName = models.CharField(
primary_key=True, db_index=True, unique=True,
verbose_name="Path/Name"
)
szJinjaCode = models.TextField(
default='', null=True, blank=True,
verbose_name='Template',
help_text='Template Code (jinja2)'
)
szDescription = models.CharField(
max_length=100,
verbose_name='Description'
)
def __unicode__(self):
return f"{self.szFileName} ({self.szDescription})"
def __str__(self):
return self.__unicode__()
def save(self, *args, **kwargs):
path_filename = TEMPLATES_DIR / self.szFileName
if not os.path.exists(os.path.dirname(path_filename)):
os.makedirs(os.path.dirname(path_filename))
with open(path_filename, "w+", encoding="utf-8") as file:
file.write(self.szJinjaCode)
# TODO: for production, need to add some code for modify
# touch_reload file for uWSGI reload
super(TbTemplate, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
pass
# ... or do smth ... and after:
# super(TbTemplate, self).delete(*args, **kwargs)
class Meta:
verbose_name = '[…Template]'
verbose_name_plural = '[…Templates]'
now, when changing and creating a template through Django-Admin, a corresponding template file will be create/modify.
Secondly,
in the admin.py file, when define the admin.ModelAdmin class for the TbTemplate control model, it is necessary to override the get_fields () method, which is responsible for getting the fields in the admin form. (I had to look for a method stupidly trying many cases that are made something similar, but not that). As a result, something like this admin.py:
# -*- coding: utf-8 -*-
from django.contrib import admin
from my_appweb.models import TbTemplate
from project.settings import *
class AdminTemplate(admin.ModelAdmin):
list_display = ('szFileName', 'szDescription')
list_display_links = ('szFileName', 'szDescription', )
def get_fields(self, request, obj=None):
try:
with open(Path(TEMPLATES_DIR) / obj.szFileName, "r", encoding="utf-8") as file:
obj.szJinjaCode = file.read()
except (AttributeError, FileNotFoundError, TypeError):
pass
return ['szFileName', 'szDescription', 'szJinjaCode']
admin.site.register(TbTemplate, AdminTemplate)
Now, if some "external forces" change the template file (or someone changes the template code in the database bypassing the admin panel), then when you open the template for editing in the admin panel, the data will still be received from the file.
It's all
P.S. in the settnig.py of the project, you need to add something like:
TEMPLATES_DIR = BASE_DIR / 'templates-jinja2'
So that the model and the admin panel know in which directory to pour the template files.
Interesting question.
The first part - inclusion tag
Your foundation - you want to use inclusion tag. Therefore you want to save something in file. But you can simply override template loader, who get before file the template from the database:
#settings.py:
TEMPLATES = [
{
'BACKEND': 'myapp.backends.MyTemplate',
... # other staff
},
]
in myapp/backends.py:
class MyTemplate(Jinja2):
def get_template(self, template_name):
template = TbTemplate.objects.filter(pk=template_name).first()
if template:
return self.from_string(template.szJinjaCode)
return super().get_template(template_name)
After that - every template can be saved in DB, {% include %} call template_backend which get template from database before file-template.
The second part. Save template to file/database.
If you do it, you don't need the first part, every time the template-file should be saved/changed.
class TbTemplateAdmin(ModelAdmin):
def save_model(self, request, obj, *args, **kwargs):
super().save(request, obj, *args, **kwargs)
with open( Path(path_to_templates) / obj.szFileName, "w+" ) as template:
template.write(obj.szJinjaCode)
The Third part - get the file in admin on get_object:
class TbTemplateAdmin(ModelAdmin):
def get_object(self, *args, **kwargs):
obj = super().get_object(self, *args, **kwargs)
with open( Path(path_to_templates) / obj.szFileName, "r" ) as template:
obj.szJinjaCode = template.read()
return obj
The Last part - convert the new file templates to objects:
In our projects we add automatically the new templates to database, spoiler - with inclusion tags. In your case - you can create an ModelAdmin.action to add templates in database. I don't solve it for you, try to do something yourself. I hope for your understanding
Only one fing I lost here. If you use Cached Template Loader, and you should use it on production, in this case you should refresh cache for changed templates. Don't forget about it.

How to use Django AutoSlugField output as ImageField filename

I am using django-extensions.AutoSlugField and django.db.models.ImageField.
To customize image name uploaded for django.db.models.ImageField, what I did:
from django.utils.text import slugify
# idea is to make image name the same as the automatically generated slug, however don't work
def update_image_name(instance, filename):
# this debug instance, and instance.slug is empty string
print(instance.__dict__)
# I attempt to use slugify directly and see that it's not the same as the output generated by AutoSlugField
# E.g. If I create display_name "shawn" 2nd time, AutoSlugField will return "shawn-2", but slugify(display_name) return "shawn"
print(slugify(instance.display_name))
return f"images/{instance.slug}.jpg"
class Object(models.Model):
...
display_name = models.TextField()
...
# to customize uploaded image name
image = models.ImageField(blank=True, upload_to=update_image_name)
...
# create slug automatically from display_name
slug = AutoSlugField(blank=True, populate_from=["display_name"]
Based on what I debug, when I call instance inside update_image_name, slug is empty string.
If I understand correctly slug is only created at event save, so when I call ImageField instance, slug is not yet created, therefore empty string.
I think it might have something to do with event post save. However, I am not sure if that's the real reason or how to do that.
How can I get the automatically generated slug as my customized image name?
That's a tricky one because the order the fields are getting saved matters.
The brute-force attack I'm suggesting would be to override the save method of your model and manually call the create_slug method before everything else ensuring the slug is set:
from django.utils.encoding import force_str
[...]
class Object(models.Model):
[...]
def save(self, *args, **kwargs):
self.slug = force_str(self._meta.get_field('slug').create_slug(self, False))
super(Object, self).save(*args, **kwargs)
That's what AutoSlugField does, refer to the code here. self._meta.get_field('slug') get's the slug field definition and then we just call the create_slug method.
Tested under Python 3.7.9 & Django 3.1.5 like this:
from django.core.files.uploadedfile import SimpleUploadedFile
o = Object()
o.display_name = "foo bar"
o.image = SimpleUploadedFile(name='test_image.png', content=open('/path/to/test/image.png', 'rb').read(), content_type='image/png')
o.save()
Then I see update_image_name return images/foo-bar.jpg.

Upload temporary file in django rest framework

I'm trying to create a way to upload xlsx files and then use celery to perform some actions.
I'm thinking this:
A view to upload the file and save it temporarily
Use celery to execute what I want in the file and then delete it.
I'm trying to do something like this:
class ImportMyFileView(APIView):
parser_classes = (FileUploadParser, )
def post(self, request, filename, format=None):
my_file = request.data["file"]
with open(f"/tmp/{my_file.name}", "wb+") as destination:
for chunk in my_file.chunks():
destination.write(chunk)
# call_celery_here()
...
Return something
I can generate the file where I want, but the problem is that when I open xlsx. I get this here:
--X-INSOMNIA-BOUNDARY
Content-Disposition: form-data
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
PK<q^Q_rels/.rels���J1��}��{w�Dd���ЛH}���a7�0u}{�Z���I~��7C��f�G�Fo�Z+���{�����kW�#�VJ$cʪ��l� �n�0�\Q�X^:�`���d�d{�m]_�d����h��V����F�w�^F9��W��-�(F/3�O�DSU�N�l/w�{N(�[��q��T����u<��r�?焮�s9�F����M��h���'h?PKf����
Is there any detail missing?
Here is how I would do it, relying on DRF's built in functionality:
import os
from rest_framework import serializers
from django.core.files.storage import FileSystemStorage
class UploadSerializer(serializers.Serializer):
file = serializers.FileField()
class UploadView(APIView):
...
def post(self, request):
ser = UploadSerializer(request.data)
ser.is_valid(raise_exception=True)
fs = FileSystemStorage(tempfile.gettempdir())
file_name = fs.save(content=ser.validated_data['file'])
full_path = os.path.join(fs.location, file_name)
celery_func.delay(file_name=full_path)
return Response("Upload OK")
A more robust way to do this would be to create a model representing your uploads to be processed, and use the django model's FileField.
class Todo(models.Model):
xlsx_file = models.FileField(...) # read the docs on this
created_at = models.DateTimeField(auto_now_add=True)
is_complete = models.BooleanField(default=False)
class UploadView(APIView):
def post(self, request):
...
todo = Todo.objects.create(
xslx_file = ser.validated_data['file']
)
celery_func.delay(todo_id=todo.pk)
return Response("Upload OK")
Once this works you can look into using a ModelSerializer either alone, or paired with a ModelViewSet. Thats a bigger learning curve though.

FilteredSelectMultiple widget on field feincms content type

I have a custom made content type in FeinCMS.
class DownloadsContent(models.Model):
title = models.CharField(max_length=200, verbose_name=_('title'))
files = FileManyToMany(verbose_name=_('files'))
The 'files' field is an manytomany which only shows .doc and .pdf files:
class FileManyToMany(models.ManyToManyField):
def __init__(self, to=MediaFile, **kwargs):
limit = {'type__in': ['doc', 'pdf']}
limit.update(kwargs.get('limit_choices_to', {}))
kwargs['limit_choices_to'] = limit
super(FileManyToMany, self).__init__(to, **kwargs)
Untill now everything works fine. When adding this content type it shows all files.
But how can I make use of the FilteredSelectMultiple widget in my content type? Like:
Actually an easier way of achieving this would be:
class DownloadContentInline(FeinCMSInline):
filter_horizontal = ['files']
class DownloadContent(models.Model):
feincms_item_editor_inline = DownloadContentInline
In my own model field class, FileManyToMany, Add "def formfield(self, ...)"
which adds the widget
from django.db import models
from feincms.module.medialibrary.models import MediaFile
class FileManyToMany(models.ManyToManyField):
def __init__(self, to=MediaFile, **kwargs):
limit = {'type__in': ['doc', 'pdf', 'xls']}
limit.update(kwargs.get('limit_choices_to', {}))
kwargs['limit_choices_to'] = limit
super(FileManyToMany, self).__init__(to, **kwargs)
def formfield(self, **kwargs):
from django.contrib import admin
defaults = {'widget': admin.widgets.FilteredSelectMultiple('vebose_name', False)}
defaults.update(kwargs)
return super(FileManyToMany, self).formfield(**defaults)

Custom filename for uploaded Image through admin interface

I have a Django model with an ImageField() field. Now I'd like to rename the filename of the image (based on a unique CharField of the same model) before it gets saved to the filesystem. Additionally, if an image with the same filename already exists, the existing file should be renamed and the newly uploaded file should keep its filename.
I am not quite sure what's the correct or preferred way to do it. Should I override ModelAdmin.save_model(), do it in the Model.save() method or write an own custom file storage?
Can anyone give me some hits how I can accomplish this? Any tips or sample code are greatly appreciated.
You can combine two mechanisms here: passing an upload_to argument in the field's definition and a custom FileSystemStorage backend.
Here is a dummy model with an upload_to callable:
def upload_bar(instance, filename):
# Do something with the filename
return new_filename
class Foo(models.Model):
bar = models.ImageField(upload_to=upload_bar)
...
And a custom dummy FileSystemStorage backend:
from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
def _save(self, name, content):
if self.exists(name):
# Rename it
return super(OverwriteStorage, self)._save(name, content)
def get_available_name(self, name):
return name
However I would be very cautious in meddling with existing files (ie changing existing files' names). Note that Django does not remove the file from the filesystem even when the object is deleted.
Here's the solution to get unique filenames like 18f6ad9f-5cec-4420-abfd-278bed78ee4a.png
models.py
import os
import uuid
from django.conf import settings
from django.db import models
def make_upload_path(instance, filename):
file_root, file_ext = os.path.splitext(filename)
dir_name = '{module}/{model}'.format(module=instance._meta.app_label, model=instance._meta.module_name)
file_root = unicode(uuid.uuid4())
name = os.path.join(settings.MEDIA_ROOT, dir_name, file_root + file_ext.lower())
# Delete existing file to overwrite it later
if instance.pk:
while os.path.exists(name):
os.remove(name)
return os.path.join(dir_name, file_root + file_ext.lower())
class YourModel(models.Model):
title = models.CharField(max_length=100)
image = models.ImageField(blank=True, upload_to=make_upload_path)
you can access self.image.name from Model.save()
http://lightbird.net/dbe/photo.html