Migrations reflect not only database but some business logic. Migrations blow up - django

Let us suppose that we had a model (example is from the documentation https://docs.djangoproject.com/en/4.1/ref/models/fields/#filefield):
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'user_{0}/{1}'.format(instance.user.id, filename)
class MyModel(models.Model):
upload = models.FileField(upload_to=user_directory_path)
Migrations will be something like:
('upload', models.FileField(upload_to=vocabulary_phrases.models.user_directory_path)),
But now you decide to change to a class:
class UploadTo:
def __init__(self, folder, filename_suffix=""):
self.folder = folder
self.filename_suffix = filename_suffix
def _get_filename(self, instance, filename):
_, file_extension = os.path.splitext(filename)
result = str(instance.a_uuid)
if self.filename_suffix:
result += "-{}".format(self.filename_suffix)
result += file_extension
return result
def save_path(self, instance, filename):
tmp_filename = self._get_filename(instance, filename)
result = "{}/{}".format(self.folder.value,
tmp_filename)
return result
class MyModel(models.Model):
upload = UploadTo(folder=UPLOAD_TO.VOCABULARY_FILE_FOLDER, filename_suffix="trololo").save_path
When you try to makemigrations, this code will blow. It will complain like 0002_migration contains user_directory_path, it is absent.
This change in the code not mechanical. I can't imagine that it can be done just by refactor / rename in IDE. Then I can't imagine what the new migration should look like. This means that I will not be able to modify migrations file easily.
I will have to deploy another project next to this one and another database. Delete all migrations, makemigrations, copy created migration and substitute all occurrances of user_directory_path with what is in my clipboard.
Complicated, I would say. And I suspect then it may be error prone.
This was just one example. Django hardcodes something in migrations which I would say is related more to business logics than to the database itself. Let us not discuss whether this example is good or not. It illustrates how something from our code is hardcoded in migrations and Django fails automatically make new migrations.
Could you recommend me what is the best practice of coping with this problem?

A simple solution would be to run
python manage.py makemigrations
while you still have user_directory_path in your file
eg:
import os
from django.db import models
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return "user_{0}/{1}".format(instance.user.id, filename)
class UploadTo:
def __init__(self, folder, filename_suffix=""):
self.folder = folder
self.filename_suffix = filename_suffix
def _get_filename(self, instance, filename):
_, file_extension = os.path.splitext(filename)
result = str(instance.a_uuid)
if self.filename_suffix:
result += "-{}".format(self.filename_suffix)
result += file_extension
return result
def save_path(self, instance, filename):
tmp_filename = self._get_filename(instance, filename)
result = "{}/{}".format(self.folder.value, tmp_filename)
return result
class UPLOAD_TO:
VOCABULARY_FILE_FOLDER = "asd"
class MyModel(models.Model):
upload = UploadTo(
folder=UPLOAD_TO.VOCABULARY_FILE_FOLDER, filename_suffix="trololo"
).save_path
then you can squash migrations and delete the function which will avoid the need to deploy a copy project

Related

Django rename uploaded file: append specific string at the end

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.

Django and SuspiciousFileOperation:Detected path traversal attempt

I find myself in an odd situation only when deployed (debug == false):
My model throws a path traversal attempt exception. I want to create a directory for every file uploaded and save the file within the directory (some.zip) used in example. In my dev environment I have no problems and everything works just fine.
models.py:
class Template(models.Model):
def get_folder(self, filename):
filename_PATH = Path(filename)
template_dir = filename_PATH.stem
return Path(settings.TEMPLATES_FOLDER).joinpath(template_dir, filename)
name = models.CharField("template", max_length=32, unique=True)
file = models.FileField("templatefile", upload_to=get_folder, null=True, max_length=260, storage=OverwriteStorage())
class OverwriteStorage(FileSystemStorage): #this is actually above
def get_available_name(self, name, max_length=None):
self.delete(name)
return name
forms.py:
class TemplateAdminForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Template
fields = ["name", "file", ]
def clean(self):
cleaned_data = super().clean()
upFile = Path(str(cleaned_data["file"]))
if upFile.suffix == ".zip":
path = self.instance.get_folder(cleaned_data["name"])
logging.error(f"{path}")
unpack_zip(path) ## works! the directory is created/filled
else:
raise forms.ValidationError("unknown file type ...")
logging.error("DONE!") # I see this output
return cleaned_data
## signal to see when the error might be happening:
#receiver(post_save, sender = Template)
def testing(sender, **kwargs):
logging.error("we never get here")
settings.py:
TEMPLATES_FOLDER = PATH(MEDIA_ROOT).joinpath("TEMPLATES")
but:
ERROR:django.security.SuspiciousFileOperation:Detected path traversal attempt in '/opt/project/media_root/TEMPLATES/some/some' WARNING:django.request:Bad Request: /admin/appName/template/add/
Edit:
Because of this discussion it might be important, this is happening on django 3.2.8
I get the same error on Django 3.2.6 when opening a file with mode "wb" at an absolute path name, when I'm not using a temporary file which I have read is recommened in order to avoid this problem so I will link this answer in case it helps you deploy it and share my experience.
Here's where it's been advised: answer
One possible solution would be to move that directory under the django project root folder and address it with a relative path. I'd try to use this too in order to understand how you could achieve this:
import os
print("WORKING DIRECTORY: " + os.getcwd())
An article on this topic suggests to use the following code (when dealing with an image file in that case): link
from django.core.files.temp import NamedTemporaryFile
from django.core import files
image_temp_file = NamedTemporaryFile(delete=True)
in_memory_image = open('/path/to/file', 'rb')
# Write the in-memory file to the temporary file
# Read the streamed image in sections
for block in in_memory_image.read(1024 * 8):
# If no more file then stop
if not block:
break # Write image block to temporary file
image_temp_file.write(block)
file_name = 'temp.png' # Choose a unique name for the file
image_temp_file.flush()
temp_file = files.File(image_temp_file, name=file_name)
Lets go through the code:
Create a NamedTemporaryFile instead of TemporaryFile as Django’s ImageField requires file name.
Iterate over your in-memory file and write blocks of data to the NamedTemporaryFile object.
Flush the file to ensure the file is written to the storage.
Change the temporary file to a Django’s File object.
You can assign this file to Django models directly and save it.
>>> from blog.models import Blog
>>> b = Blog.objects.first()
>>> b.image = temp_file
>>> b.save()
I personally solved my SuspiciousFileOperation problems by addressing my directory with "BASE_DIR" from settings.py as the beginning of the path (nothing above that level in the filesystem), using a NamedTemporaryFile and by using the model FileField save() method appropriately like this:
# inside a model class save(self, *args, **kwargs) method
# file_name is the file name alone, no path to the file
self.myfilefield.save(file_name, temporary_file_object, save=False) # and then call the super().save(*args, **kwargs) inside the save() method of your model

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.

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

How do I get Django Admin to delete files when I remove an object from the database/model?

I am using 1.2.5 with a standard ImageField and using the built-in storage backend. Files upload fine but when I remove an entry from admin the actual file on the server does not delete.
You can receive the pre_delete or post_delete signal (see #toto_tico's comment below) and call the delete() method on the FileField object, thus (in models.py):
class MyModel(models.Model):
file = models.FileField()
...
# Receive the pre_delete signal and delete the file associated with the model instance.
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver
#receiver(pre_delete, sender=MyModel)
def mymodel_delete(sender, instance, **kwargs):
# Pass false so FileField doesn't save the model.
instance.file.delete(False)
Try django-cleanup
pip install django-cleanup
settings.py
INSTALLED_APPS = (
...
'django_cleanup.apps.CleanupConfig',
)
Django 1.5 solution: I use post_delete for various reasons that are internal to my app.
from django.db.models.signals import post_delete
from django.dispatch import receiver
#receiver(post_delete, sender=Photo)
def photo_post_delete_handler(sender, **kwargs):
photo = kwargs['instance']
storage, path = photo.original_image.storage, photo.original_image.path
storage.delete(path)
I stuck this at the bottom of the models.py file.
the original_image field is the ImageField in my Photo model.
This code runs well on Django 1.4 also with the Admin panel.
class ImageModel(models.Model):
image = ImageField(...)
def delete(self, *args, **kwargs):
# You have to prepare what you need before delete the model
storage, path = self.image.storage, self.image.path
# Delete the model before the file
super(ImageModel, self).delete(*args, **kwargs)
# Delete the file after the model
storage.delete(path)
It's important to get the storage and the path before delete the model or the latter will persist void also if deleted.
You need to remove the actual file on both delete and update.
from django.db import models
class MyImageModel(models.Model):
image = models.ImageField(upload_to='images')
def remove_on_image_update(self):
try:
# is the object in the database yet?
obj = MyImageModel.objects.get(id=self.id)
except MyImageModel.DoesNotExist:
# object is not in db, nothing to worry about
return
# is the save due to an update of the actual image file?
if obj.image and self.image and obj.image != self.image:
# delete the old image file from the storage in favor of the new file
obj.image.delete()
def delete(self, *args, **kwargs):
# object is being removed from db, remove the file from storage first
self.image.delete()
return super(MyImageModel, self).delete(*args, **kwargs)
def save(self, *args, **kwargs):
# object is possibly being updated, if so, clean up.
self.remove_on_image_update()
return super(MyImageModel, self).save(*args, **kwargs)
You may consider using a pre_delete or post_delete signal:
https://docs.djangoproject.com/en/dev/topics/signals/
Of course, the same reasons that FileField automatic deletion was removed also apply here. If you delete a file that is referenced somewhere else you will have problems.
In my case this seemed appropriate because I had a dedicated File model to manage all of my files.
Note: For some reason post_delete doesn't seem to work right. The file got deleted, but the database record stayed, which is completely the opposite of what I would expect, even under error conditions. pre_delete works fine though.
Maybe it's a little late. But the easiest way for me is to use a post_save signal. Just to remember that signals are excecuted even during a QuerySet delete process, but the [model].delete() method is not excecuted during the QuerySet delete process, so it's not the best option to override it.
core/models.py:
from django.db import models
from django.db.models.signals import post_delete
from core.signals import delete_image_slide
SLIDE1_IMGS = 'slide1_imgs/'
class Slide1(models.Model):
title = models.CharField(max_length = 200)
description = models.CharField(max_length = 200)
image = models.ImageField(upload_to = SLIDE1_IMGS, null = True, blank = True)
video_embed = models.TextField(null = True, blank = True)
enabled = models.BooleanField(default = True)
"""---------------------------- SLIDE 1 -------------------------------------"""
post_delete.connect(delete_image_slide, Slide1)
"""--------------------------------------------------------------------------"""
core/signals.py
import os
def delete_image_slide(sender, **kwargs):
slide = kwargs.get('instance')
try:
os.remove(slide.image.path)
except:
pass
This functionality will be removed in Django 1.3 so I wouldn't rely on it.
You could override the delete method of the model in question to delete the file before removing the entry from the database completely.
Edit:
Here is a quick example.
class MyModel(models.Model):
self.somefile = models.FileField(...)
def delete(self, *args, **kwargs):
somefile.delete()
super(MyModel, self).delete(*args, **kwargs)
Using the post_delete is for sure the right way to go. Sometimes though things can go wrong, and files don't get deleted. There is of course the case that you have a bunch of old files that weren't deleted before post_delete was used. I created a function that deletes files for objects based on if the file the object references does not exist then delete object, if the file does not have an object, then also delete, also it can delete based on an "active" flag for an object.. Something I added to most of my models. You have to pass it the objects you want to check, the path to the objects files, the file field and a flag to delete inactive objects:
def cleanup_model_objects(m_objects, model_path, file_field='image', clear_inactive=False):
# PART 1 ------------------------- INVALID OBJECTS
#Creates photo_file list based on photo path, takes all files there
model_path_list = os.listdir(model_path)
#Gets photo image path for each photo object
model_files = list()
invalid_files = list()
valid_files = list()
for obj in m_objects:
exec("f = ntpath.basename(obj." + file_field + ".path)") # select the appropriate file/image field
model_files.append(f) # Checks for valid and invalid objects (using file path)
if f not in model_path_list:
invalid_files.append(f)
obj.delete()
else:
valid_files.append(f)
print "Total objects", len(model_files)
print "Valid objects:", len(valid_files)
print "Objects without file deleted:", len(invalid_files)
# PART 2 ------------------------- INVALID FILES
print "Files in model file path:", len(model_path_list)
#Checks for valid and invalid files
invalid_files = list()
valid_files = list()
for f in model_path_list:
if f not in model_files:
invalid_files.append(f)
else:
valid_files.append(f)
print "Valid files:", len(valid_files)
print "Files without model object to delete:", len(invalid_files)
for f in invalid_files:
os.unlink(os.path.join(model_path, f))
# PART 3 ------------------------- INACTIVE PHOTOS
if clear_inactive:
#inactive_photos = Photo.objects.filter(active=False)
inactive_objects = m_objects.filter(active=False)
print "Inactive Objects to Delete:", inactive_objects.count()
for obj in inactive_objects:
obj.delete()
print "Done cleaning model."
This is how you can use this:
photos = Photo.objects.all()
photos_path, tail = ntpath.split(photos[0].image.path) # Gets dir of photos path, this may be different for you
print "Photos -------------->"
cleanup_model_objects(photos, photos_path, file_field='image', clear_inactive=False) # image file is default
make sure you write "self" before the file. so example above should be
def delete(self, *args, **kwargs):
self.somefile.delete()
super(MyModel, self).delete(*args, **kwargs)
I've forgotten the "self" before my file and that didn't work as it was looking in the global namespace.
If you already have number of unused files in your project and want to delete them, you can use django utility django-unused-media
Django 2.x Solution:
There's no need to install any packages! It's very easy to handle in Django 2. I've tried following solution using Django 2 and SFTP Storage (however I think it would work with any storages)
First write a Custom Manager. So if you want to be able to delete files of a model by using objects methods, you must write and use a [Custom Manager][3] (for overriding delete() method of objects):
class CustomManager(models.Manager):
def delete(self):
for obj in self.get_queryset():
obj.delete()
Now you must delete image before deleting deleting the model itself and for assigning the CustomManager to the model, you must initial objects inside your model:
class MyModel(models.Model):
image = models.ImageField(upload_to='/pictures/', blank=True)
objects = CustomManager() # add CustomManager to model
def delete(self, using=None, keep_parents=False):
objects = CustomManager() # just add this line of code inside of your model
def delete(self, using=None, keep_parents=False):
self.image.storage.delete(self.song.name)
super().delete()
I may have a special case since I am using the upload_to option on my file field with dynamic directory names but the solution I found was to use os.rmdir.
In models:
import os
...
class Some_Model(models.Model):
save_path = models.CharField(max_length=50)
...
def delete(self, *args,**kwargs):
os.rmdir(os.path.join(settings.MEDIA_ROOT, self.save_path)
super(Some_Model,self).delete(*args, **kwargs)