django instance.id=None when uploading image - python

instance.id is returning None when upload images through the admin page. The idea was to upload all the images of each Residence to a different folder. Here's my code:
models.py
from django.db import models
import os
def get_image_path(instance, filename):
return os.path.join('photos', "residence_%s" % instance.id, filename)
# Create your models here.
class Residence(models.Model):
big_image = models.ImageField("Main Image",upload_to=get_image_path)
small_images = models.ImageField("Small Images",upload_to=get_image_path, blank=True, null=True)
settings.py
MEDIA_URL = '/media/'
EDIT: It works if I modify the image after the model is already added.

You can't do it in that way unless you implement your custom dynamic file upload field. Because you try to access instance.id, but instance isn't saved yet and doesn't have an id.
Here you are some resources that will help you achieve what you want:
dynamic file upload path with current instance id
Django admin file upload with current model id

Another nice way to solve this problem that requires much less code is to have your model use a UUID for the primary key rather then the database generated id. This means at the point the model is saved for the first time the UUID is already known and can be used with any upload_to callbacks.
So for the original example you would do something like this
from django.db import models
import uuid
import os
def get_image_path(instance, filename):
return os.path.join('photos', "residence_%s" % str(instance.id), filename)
# Create your models here.
class Residence(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
big_image = models.ImageField("Main Image",upload_to=get_image_path)
small_images = models.ImageField("Small Images",upload_to=get_image_path, blank=True, null=True)
See Django's UUIDField reference for more info

If you don't specify an update_to on the ImageField you could upload to your media root then change the path using a post_save signal.
#receiver(post_save, sender=Product)
def update_file_path(instance, created, **kwargs):
if created:
initial_path = instance.image.path
new_path = settings.MEDIA_ROOT + f'/product_{instance.id}/{instance.image.name}'
os.makedirs(os.path.dirname(new_path), exist_ok=True)
os.rename(initial_path, new_path)
instance.image = new_path
instance.save()

you can create a model instance by passing cleaned data from your form as **kwargs to django model i did it that way & its much easier than anything else
in your views post method add this (this code is from my project not adapted to this question)
pk = request.session['_auth_user_id']
user_obj = User.objects.get(pk=pk)
lab_form_instance = lab_form(request.POST,request.FILES)
lab_form_instance.save(commit=False)
# here you can put the form.is_valid() statement
lab_form_instance.cleaned_data['owner'] =user_obj # here iam adding additional needed data for the model
obj = lab(**lab_form_instance.cleaned_data)
obj.save()

Related

Unique filename of uploaded file using the django FORM

I'm trying to generate a unique filename for the uploaded file using the Django forms. I've tried uuid_upload_path app but that app doesn't work with the form. Below is my code
Forms.py
class HelpGuideForm(forms.ModelForm):
title = forms.CharField(max_length = 50)
image = forms.ImageField(required = False)
class Meta:
model = Helpguide
fields = ['title', 'image']
Models.py
from uuid_upload_path import upload_to
class HelpguideImage(models.Model):
image = models.ImageField(upload_to = upload_to, blank=True, null=True)
I want a unique name for all uploaded files. something like sd564sadasd61.jpg. I'm using Django 2.2
In your Model you can set the upload_to of the imagefield to a function and then generate the uuid.
A very simple (untested) example:
import uuid
Class MyModel(models.Model):
def get_path(instance, filename):
extension = filename.split('.')[-1]
uuid = uuid.uuid1().hex
return f'path/to/file/{uuid}.{extension}'
image = ImageField(upload_to=get_path)
What I understand of your problem, you can set initial for FORM class when initialising it. like:
help_guide_form = HelpGuideForm(initial={'headline': uuid.uuid4().hex}, instance= Helpguide)
from django docs. Also see the initial reference.

How to override Model defaults method of 3rd party installed app, django?

I just installed this django-csvimport package. Now I want to override the default values in the Admin area form. I found the code here, which defines the models, and contains the current default text:
class CSVImport(models.Model):
""" Logging model for importing files """
model_choice = []
model_name = models.CharField(max_length=255, blank=False,
default='csvimport.Item',
help_text='Please specify the app_label.model_name',
choices=get_models())
field_list = models.TextField(blank=True,
help_text='''Enter list of fields in order only if
you dont have a header row with matching field names, eg.
"column1=shared_code,column2=org(Organisation|name)"''')
upload_file = models.FileField(upload_to='csv', storage=fs)
file_name = models.CharField(max_length=255, blank=True)
encoding = models.CharField(max_length=32, blank=True)
upload_method = models.CharField(blank=False, max_length=50,
default='manual', choices=CHOICES)
error_log = models.TextField(help_text='Each line is an import error')
import_date = models.DateField(auto_now=True)
import_user = models.CharField(max_length=255, default='anonymous',
help_text='User id as text', blank=True)
def error_log_html(self):
return re.sub('\n', '<br/>', self.error_log)
error_log_html.allow_tags = True
def __unicode__(self):
return self.upload_file.name
So for example I would like override the model_name field default csvimport.Item with something else. I am a bit at a loss how to override this as I do not have an app folder for csvimport, as its a 3rd part installation. It will be my first time overriding a 3rd party installed app model.
Now that I look into it a bit more, not sure if I should override this model or perhaps better the ModelAdmin class of the admin.py file?
Thanks!
"""Your admin.py"""
from csvimport.models import CSVImport
from csvimport.admin import CSVImportAdmin
class MyCSVImportAdmin(CSVImportAdmin):
"""Override some of the form's field properties:
clean, creation_counter, default_error_messages,
default_validators, disabled, empty_value, empty_values .. etc
"""
def get_form(self, request, obj=None, **kwargs):
form = super(MyCSVImportAdmin, self).get_form(request, obj, **kwargs)
form.base_fields["model_name"].initial = 'What you want'
form.base_fields["model_name"].help_text = 'Please customize the fields however you like'
return form
admin.site.unregister(CSVImport)
admin.site.register(CSVImport, MyCSVImportAdmin)
I saw the whole code and django-csvimport package does not provide you the functionality to override anything from their code so its not possible to override without copying app to your project. Below is an example of another app django-oauth-toolkit which uses a user settings param to provide the functionality of modifications.
USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None)
Now the solution would be only to copy the app and then modify the app for your own usage.

Django: Save user uploads in seperate folders

I want individual users to be able to upload their files into a single folder (so each user has their own root folder, where they can upload their own files), but I am not sure of the best way to go about implementing this.
I originally planned to use the users email as the their folder name, and all their uploads would be saved in this folder. However, from what I have gathered the only way of retrieving this information is through the request function, and I cannot manage to get an instance of request into the models.py file, and therefore cannot add it to my 'upload_to' directory.
Any other ideas of how to separate users files, or how to get an instance of request in the models file would be greatly appreciated!!
Here is my current model:
def user_directory_path(instance, filename):
return 'user_{0}/{1}'.format(instance.user.id, filename)
class UploadModel(models.Model):
user = models.OneToOneField(User)
file = models.FileField(upload_to=user_directory_path)
And it's associated error:
Exception Value: UploadModel has no user.
I don't recommend you to use user email or any other information that can be updated as folder name because you won't change folder name each time he changes his email or his username. So, use user id that is unique and unchangeable.
Here is a complete example from Django documentation, to access instance information in your models to build path with user id :
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)
In this case, it use the user id in the folder name. Of course, your can replaceFileField with ImageField.
More information in django docs : FileFields.upload_to
You could maybe separate the folder by their username? You can create a function that would create a folder using the users username like so:
def get_user_image_folder(instance, filename):
return "%s/%s" %(instance.user.username, filename)
and in your model you could easily use the upload_to and add the function that you just created to it:
class Images(models.Model):
user = models.ForeignKey(User)
image = models.ImageField(upload_to=get_user_image_folder,
verbose_name='Image', )
You don't have to use request in Models, you use instance instead.
To get this to work I implemented the solution from the docs as suggested by Louis Barranqueiro, whereby the models looks like:
# models.py
def user_directory_path(instance, filename):
return 'user_{0}/{1}'.format(instance.user.id, filename)
class Document(models.Model):
file = models.FileField(upload_to=user_directory_path)
uploaded_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='documents')
But crucially, I also changed my DocumentUploadView class to include a save step, so that the document saves with the user attribute (note also the initial commit=False save step, which is crucial):
# views.py
class DocumentUploadView(View):
def get(self, request):
documents_list = Document.objects.all()
return render(self.request, 'file_upload.html', {'documents': documents_list})
def post(self, request):
form = DocumentForm(self.request.POST, self.request.FILES)
if form.is_valid():
document = form.save(commit=False)
document.user = request.user
document.save()
data = {'is_valid': True, 'name': document.file.name, 'url': document.file.url}
else:
data = {'is_valid': False}
return JsonResponse(data)
Finally my forms.py looks like this:
# forms.py
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ('file',)
For anyone else in the future that stumble across this, I got access to the current user's ID by adding the user object to the model in the view.
views.py
from .models import Document
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
newdoc = Document(docfile=request.FILES['docfile'])
newdoc.owner = request.user
newdoc.save()
Then, in models.py you can retrieve the ID from owner created previously in view.
def user_directory_path(instance, filename):
return 'Users/user_{0}/{1}'.format(instance.owner.id, filename)
class Document(models.Model):
docfile = models.FileField(upload_to=user_directory_path)
In your views.py you must pass the instance argument like this:
def post(self, request, *args, **kwargs):
if 'uploadFile' in request.POST:
f = UploadFileForm(request.POST, request.FILES, instance=request.user.uploadmodel)
if f.is_valid():
f.save()
return HttpResponseRedirect('/sucess_url')
And your forms.py
from django import forms
from .models import UploadForm
class UploadFileForm(forms.ModelForm):
class Meta:
model = UploadForm
fields = ('file',)
I also faced the same problem. Here is how I solved it. We have to create the user referencing in the view.
Below is the code for models.py
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 Document(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE)
docfile = models.FileField(upload_to=user_directory_path)
date = models.DateTimeField(auto_now_add=True, blank=True)
Also update the code in views.py as below:
def upload(request):
# Handle file upload
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
newdoc = Document(docfile=request.FILES['docfile'],user=request.user)
newdoc.save()
latest_documents = Document.objects.all().order_by('-id')[0]
# Redirect to the document list after POST
return HttpResponseRedirect(reverse('list'))
else:
form = DocumentForm() # A empty, unbound form
# Load documents for the list page
documents = Document.objects.filter(user=request.user)
The most important one is in the line,
newdoc = Document(docfile=request.FILES['docfile'],user=request.user)
Somehow I could not get the model instance to read the user. This approach is working for me.

Django - Accessing attributes through a OneToOneField

I am very new to Django and I was wondering if I could request some help with an issue I am facing. I'm trying to build a set of models in Django that is structured as follows:
An app_user describes a user of the application I am building.
An app_job describes a job that the user wants to run on the app, with several associated inputs (upload1, upload2, upload3). A user can run many jobs; hence, I use the many-to-one (ForeignKey) relationship between job and user. When an app_job is created, I want its associated files to be uploaded to a directory determined by the associated user's username and num_jobs attribute, as shown.
When I run python manage.py makemigrations, I receive the following error: AttributeError: 'ForeignKey' object has no attribute 'user'. Which begs the question, how can I access the underlying app_user's information from the app_job class?
Thanks for the help.
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.forms import ModelForm
class app_user(models.Model):
user = models.OneToOneField(User)
num_jobs = models.IntegerField(default = 0)
def __unicode__(self):
return self.user.get_username()
class app_job(models.Model):
app_user = models.ForeignKey(app_user)
upload1 = models.FileField(upload_to = app_user.user.get_username() + "/" + str(app_user.num_jobs) , blank = True, null = True)
upload2 = models.FileField(upload_to = app_user.user.get_username() + "/" + str(app_user.num_jobs))
upload3 = models.FileField(upload_to = app_user.user.get_username() + "/" + str(app_user.num_jobs) )
Okay there are a couple of issues here. But nothing a little education cannot fix.
models should be CamelCased this makes it easier to read and is generally good practise.
You do not need to prefix models with app_ its much cleaner and easier to read without this.
Anyway,
You app_job model ForeignKey should be to User not to the app_user model. By doing this you can still gain access to the app_user data.
You also need to modify the upload_to attributes also. Whilst uploads_to can be a string value it cannot be evaluated the way you are currently doing this. Check out the django filefield documentation for details (shameless plug, I recently re-wrote this part of the documentation).
Instead you need to do the following:
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 app_job(models.Model):
app_user = models.ForeignKey(User)
upload1 = models.FileField(upload_to=user_directory_path , blank = True, null = True)
upload2 = models.FileField(upload_to=user_directory_path
upload3 = models.FileField(upload_to=user_directory_path, blank=True, null=True)
What this is doing is upload_to calls the function user_directory_path to generate the file path.
By following the above you should have something like:
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.forms import ModelForm
class UserProfile(models.Model):
"""
In settings.py you will want to add a link to AUTH_USER_PROFILE
See https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#extending-the-existing-user-model
"""
user = models.OneToOneField(User)
num_jobs = models.IntegerField(default=0)
def __unicode__(self):
return self.user.get_username()
def upload_dir(instance, filename):
return instance.user.get_username() + "/" + str(instance.user.num_jobs)
class Job(models.Model):
user = models.ForeignKey(User)
upload1 = models.FileField(upload_to=upload_dir, blank = True, null = True)
upload2 = models.FileField(upload_to=upload_dir, blank=True, null=True)
upload3 = models.FileField(upload_to=upload_dir, blank=True, null=True)
Based on this answer to a similar question something like this should work:
# models.py
from django.db import models
from django.contrib.auth.models import User
class app_user(models.Model):
user = models.OneToOneField(User)
num_jobs = models.IntegerField(default = 0)
def __unicode__(self):
return self.user.get_username()
def content_file_name(instance, filename):
return '/'.join([instance.app_user.user.get_username(), str(instance.app_user.num_jobs), filename])
class app_job(models.Model):
app_user = models.ForeignKey(app_user)
upload1 = models.FileField(upload_to = content_file_name , blank = True, null = True)
upload2 = models.FileField(upload_to = content_file_name)
upload3 = models.FileField(upload_to = content_file_name)
As you have been told there are several issues. But its good to learn :).
First the class name should start with an upper letter and use "camelCase",
in your case, "AppUser" and "AppJob". Personally I would not use "App" as suffix instead I would just use "MyUser" and "Job".
The error you are getting "'ForeignKey' object has no attribute 'user" is because in your "app_job" model you have a ForeignKey to app_user, which is OK, that creates the relationship between the both models, but then in the FileField's you are using that "foreignkey" object which have the same name as your model "app_user" and that ForeignKey instance does not have an attribute called "user", Do you get this?
The information the guys gave you related to the "upload_to" is correct :).
you can get more info in the Django docs
Thanks

Django - Delete file from amazon S3

I have a problem where deleting an object form the admin won't delete the file associated with it. after some research I decided to implement a post_delete in the model.
For some reason I am not able to make the s3 delete the file, even after searching numerous guides and snippets, maybe someone here knows.
I use django 1.5 and boto.
Heres my code for the model:
from django.db import models
from django.contrib.auth.models import User
from fileservice.formatChecker import ContentTypeRestrictedFileField
from south.modelsinspector import add_introspection_rules
import os
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.core.files.storage import default_storage as storage
add_introspection_rules([
(
[ContentTypeRestrictedFileField], # Class(es) these apply to
[], # Positional arguments (not used)
{ # Keyword argument
"content_types": ["content_types", {}],
"max_upload_size": ["max_upload_size", {}]
},
),
], ["^fileservice\.formatChecker\.ContentTypeRestrictedFileField"])
class Contentfile(models.Model):
content = ContentTypeRestrictedFileField(upload_to='uploads/', content_types=['video/mp4', 'application/pdf', 'image/gif', 'image/jpeg', 'image/png'],max_upload_size=5242880,blank=True, null=True, help_text='Upload a file to add it to the content the app displayes')
created_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
title = models.CharField(max_length=255, unique=True)
file_type = models.CharField(max_length=5)
published = models.BooleanField(default=True)
file_owner = models.ForeignKey(User, related_name='Contentfiles')
class Meta:
ordering = ["title"]
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
file_name = os.path.basename(self.content.name)
self.file_type = file_name.split('.')[-1]
self.title = file_name.split('.')[0]
self.published = True
super(Contentfile, self).save(*args, **kwargs)
#receiver(models.signals.post_delete, sender=Contentfile)
def auto_delete_file_on_delete(sender, instance, **kwargs):
"""Deletes file from filesystem
when corresponding `MediaFile` object is deleted.
"""
if instance.content:
if os.path.isfile(storage.open(instance.content.path)):
os.remove(storage.open(instance.content.path))
#receiver(models.signals.pre_save, sender=Contentfile)
def auto_delete_file_on_change(sender, instance, **kwargs):
"""Deletes file from filesystem
when corresponding `MediaFile` object is changed.
"""
if not instance.pk:
return False
try:
old_file = Contentfile.objects.get(pk=instance.pk).content
except Conentfile.DoesNotExist:
return False
new_file = instance.content
if not old_file == new_file:
if os.path.isfile(storage.open(old_file.path)):
os.remove(storage.open(old_file.path))
It is MUCH safer to do post_delete. If something goes wrong you will start missing S3 files and you wont notice it because your DB records are intact. post_delete will be safer since it is less likely that S3 delete operation would fail after you have deleted your db record. Furthermore even if file delete fails you will be left with a bunch of unreferenced S3 file which are harmless and can be easily cleaned up.
#receiver(models.signals.post_delete, sender=Picture)
def remove_file_from_s3(sender, instance, using, **kwargs):
instance.img.delete(save=False)
You need to call FieldFile's delete() method to remove the file in S3. In your case, add a pre_delete signal where you call it:
#receiver(models.signals.pre_delete, sender=ContentFile)
def remove_file_from_s3(sender, instance, using):
instance.content.delete(save=False)
Try django-cleanup, it automatically invokes delete method on FileField when you remove model.
This worked for me by deleting files both in DB and in AWS S3.
from django.db import models
from django.dispatch import receiver
from django.views import generic
from project.models import ContentFile
from django.contrib.auth.mixins import LoginRequiredMixin,UserPassesTestMixin
class DeleteFileView(LoginRequiredMixin,UserPassesTestMixin,generic.DeleteView):
model = ContentFile
template_name = 'file-delete.html'
success_url = 'redirect-to'
#Check if the owner of the file is the one trying to delete a file
def test_func(self):
obj = self.get_object()
if obj.user == self.request.user:
return True
return False
#This code does magic for S3 file deletion
#receiver(models.signals.pre_delete, sender=ContentFile)
def remove_file_from_s3(sender, instance, using, **kwargs):
instance.image_file.delete(save=False)
I am using pre_delete you can check the django documentation.File reference deletion in DB is done by DeleteView, I hope this helps someone

Categories

Resources