Django: Access primary key in models.filefield(upload_to) location - python

I'd like to save my files using the primary key of the entry.
Here is my code:
def get_nzb_filename(instance, filename):
if not instance.pk:
instance.save() # Does not work.
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (instance.pk, name_slug)
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
I know the first time an object is saved the primary key isn't available, so I'm willing to take the extra hit to save the object just to get the primary key, and then continue on.
The above code doesn't work. It throws the following error:
maximum recursion depth exceeded while calling a Python object
I'm assuming this is an infinite loop. Calling the save method would call the get_nzb_filename method, which would again call the save method, and so on.
I'm using the latest version of the Django trunk.
How can I get the primary key so I can use it to save my uploaded files?
Update #muhuk:
I like your solution. Can you help me implement it? I've updated my code to the following and the error is 'File' object has no attribute 'create'. Perhaps I'm using what you've written out of context?
def create_with_pk(self):
instance = self.create()
instance.save()
return instance
def get_nzb_filename(instance, filename):
if not instance.pk:
create_with_pk(instance)
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (instance.pk, name_slug)
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename, blank=True, null=True)
name = models.CharField(max_length=256)
Instead of enforcing the required field in my model I'll do it in my Form class. No problem.

It seems you'll need to pre-generate your File models with empty file fields first. Then pick up one and save it with the given file object.
You can have a custom manager method like this;
def create_with_pk(self):
instance = self.create()
instance.save() # probably this line is unneeded
return instance
But this will be troublesome if either of your fields is required. Because you are initially creating a null object, you can't enforce required fields on the model level.
EDIT
create_with_pk is supposed to be a custom manager method, in your code it is just a regular method. Hence self is meaningless. It is all properly documented with examples.

You can do this by setting upload_to to a temporary location and by creating a custom save method.
The save method should call super first, to generate the primary key (this will save the file to the temporary location). Then you can rename the file using the primary key and move it to it's proper location. Call super one more time to save the changes and you are good to go! This worked well for me when I came across this exact issue.
For example:
class File( models.Model ):
nzb = models.FileField( upload_to='temp' )
def save( self, *args, **kwargs ):
# Call save first, to create a primary key
super( File, self ).save( *args, **kwargs )
nzb = self.nzb
if nzb:
# Create new filename, using primary key and file extension
oldfile = self.nzb.name
dot = oldfile.rfind( '.' )
newfile = str( self.pk ) + oldfile[dot:]
# Create new file and remove old one
if newfile != oldfile:
self.nzb.storage.delete( newfile )
self.nzb.storage.save( newfile, nzb )
self.nzb.name = newfile
self.nzb.close()
self.nzb.storage.delete( oldfile )
# Save again to keep changes
super( File, self ).save( *args, **kwargs )

Context
Had the same issue.
Solved it attributing an id to the current object by saving the object first.
Method
create a custom upload_to function
detect if object has pk
if not, save instance first, retrieve the pk and assign it to the object
generate your path with that
Sample working code :
class Image(models.Model):
def upload_path(self, filename):
if not self.pk:
i = Image.objects.create()
self.id = self.pk = i.id
return "my/path/%s" % str(self.id)
file = models.ImageField(upload_to=upload_path)

You can create pre_save and post_save signals. Actual file saving will be in post_save, when pk is already created.
Do not forget to include signals in app.py so they work.
Here is an example:
_UNSAVED_FILE_FIELD = 'unsaved_file'
#receiver(pre_save, sender=File)
def skip_saving_file_field(sender, instance: File, **kwargs):
if not instance.pk and not hasattr(instance, _UNSAVED_FILE_FIELD):
setattr(instance, _UNSAVED_FILE_FIELD, instance.image)
instance.nzb = None
#receiver(post_save, sender=File)
def save_file_field(sender, instance: Icon, created, **kwargs):
if created and hasattr(instance, _UNSAVED_FILE_FIELD):
instance.nzb = getattr(instance, _UNSAVED_FILE_FIELD)
instance.save()

Here are 2 possible solutions:
Retrieve id before inserting a row
For simplicity I use postgresql db, although it is possible to adjust implementation for your db backend.
By default django creates id as bigserial (or serial depending on DEFAULT_AUTO_FIELD). For example, this model:
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
Produces the following DDL:
CREATE TABLE "example_file" ("id" bigserial NOT NULL PRIMARY KEY, "nzb" varchar(100) NOT NULL, "name" varchar(256) NOT NULL);
There is no explicit sequence specification. By default bigserial creates sequence name in the form of tablename_colname_seq (example_file_id_seq in our case)
The solution is to retrieve this id using nextval :
def get_nextval(model, using=None):
seq_name = f"{model._meta.db_table}_id_seq"
if using is None:
using = "default"
with connections[using].cursor() as cursor:
cursor.execute("select nextval(%s)", [seq_name])
return cursor.fetchone()[0]
And set it before saving the model:
class File(models.Model):
# fields definition
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if not self.pk:
self.pk = get_nextval(self, using=using)
force_insert = True
super().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
Note that we rely on force_insert behavior, so make sure to read documentation and cover your code with tests:
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ModelForm
from django.test import TestCase
from example import models
class FileForm(ModelForm):
class Meta:
model = models.File
fields = (
"nzb",
"name",
)
class FileTest(TestCase):
def test(self):
form = FileForm(
{
"name": "picture",
},
{
"nzb": SimpleUploadedFile("filename", b"content"),
},
)
self.assertTrue(form.is_valid())
form.save()
self.assertEqual(models.File.objects.count(), 1)
f = models.File.objects.first()
self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
Insert without nzt then update with actual nzt value
The idea is self-explanatory - we basically pop nzt on the object creation and save object again after we know id:
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
nzb = None
if not self.pk:
nzb = self.nzb
self.nzb = None
super().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
if nzb:
self.nzb = nzb
super().save(
force_insert=False,
force_update=True,
using=using,
update_fields=["nzb"],
)
Test is updated to check actual queries:
def test(self):
form = FileForm(
{
"name": "picture",
},
{
"nzb": SimpleUploadedFile("filename", b"content"),
},
)
self.assertTrue(form.is_valid())
with CaptureQueriesContext(connection) as ctx:
form.save()
self.assertEqual(models.File.objects.count(), 1)
f = models.File.objects.first()
self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
self.assertEqual(len(ctx.captured_queries), 2)
insert, update = ctx.captured_queries
self.assertEqual(
insert["sql"],
'''INSERT INTO "example_file" ("nzb", "name") VALUES ('', 'picture') RETURNING "example_file"."id"''',
)
self.assertRegexpMatches(
update["sql"],
rf"""UPDATE "example_file" SET "nzb" = 'files/{f.pk}_picture(.*)\.nzb' WHERE "example_file"."id" = {f.pk}""",
)

Ty, is there a reason you rolled your own slugify filter?
Django ships with a built-in slugify filter, you can use it like so:
from django.template.defaultfilters import slugify
slug = slugify(some_string)
Not sure if you were aware it was available to use...

You can use the next available primary key ID:
class Document(models.Model):
def upload_path(self, filename):
if not self.pk:
document_next_id = Document.objects.order_by('-id').first().id + 1
self.id = self.pk = document_next_id
return "my/path/document-%s" % str(self.pk)
document = models.FileField(upload_to=upload_path)
Details
My example is a modification of #vinyll's answer, however, the problem Giles mentioned in his comment (two objects being created) is resolved here.
I am aware that my answer is not perfect, and there can be issues with the "next available ID", e.g., when more users will attempt to submit many forms at once. Giles's answer is more robust, mine is simpler (no need to generate temp files, then moving files, and deleting them). For simpler applications, this will be enough.
Also credits to Tjorriemorrie for the clear example on how to get the next available ID of an object.

Well I'm not sure of my answer but -
use nested models, if you can -
class File(models.Model):
name = models.CharField(max_length=256)
class FileName(models.Model):
def get_nzb_filename(instance, filename):
return instance.name
name = models.ForeignKey(File)
nzb = models.FileField(upload_to=get_nzb_filename)
And in create method -
File_name = validated_data.pop(file_name_data)
file = File.objects.create(validated_data)
F = FileName.objects.create(name=file, **File_name)

Related

Dynamic JsonForm with ImageField in Django

I am creating a dynamic form (using JSON format) in Django, the form needs to save multiple images using Django Storage and save the reference to the file in the JSON field.
This is what I have right now, it works but is really ugly, Django already do this, I can't figure out how to re-use same functionality.
Class SomeModel(models.ModelForm):
results = JSONField()
class DynamicJsonForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# ... Add dynamic fields to the form
self.extra = [] # Save the reference for the fields
class Meta:
model = SomeModel
exclude = ("results",)
def save(self, commit=True):
results = {}
for extra in self.extra:
value = self.cleaned_data.get(extra)
question = some_query.question
if "photo" in extra and value: # value = photo
filename, ext = value.name.split(".")
filename = "media/the/path/to/photos/{}_{}.{}".format(filename, uuid4().hex, ext)
uploaded_file = SimpleUploadedFile(filename, value.read(), value.content_type)
image = Image.open(uploaded_file)
if image.mode in ("RGBA", "P"):
image = image.convert("RGB")
image.save(fp=filename)
results[question][extra] = filename
else:
results[question][extra] = value
self.instance.results = results
return super().save(commit)
This actually works, it saves the data to the JSONField (results) and saves the image to local file system storage.
How can this be improved to use Django Storage and make it simple? Using Django Model.save() seems a lot easier, but I can't use a normal ImageField() since it needs to be dynamic
I mixed FileSystemStorage with forms.ImageField, not a perfect solution at all, but looks better.
fields.py
import os
from django import forms
from django.conf import settings
from django.core.files.storage import FileSystemStorage
class FormFileSystemStorageImageField(forms.ImageField, FileSystemStorage):
def __init__(self, location=None, *args, **kwargs):
super().__init__(*args, **kwargs) # Call ImageField __init__, I wonder how to call second parent's __init__
self._orig_location = location
self._location = os.path.join(settings.MEDIA_ROOT, location)
def storage_path(self, name):
return os.path.join(self._orig_location, name)
forms.py
from .fields import FormFileSystemStorageImageField
# ... Same as question code
def save(self, commit=True):
results = {}
for extra in self.extra:
value = self.cleaned_data.get(extra)
question = some_query.question
if "photo" in extra and value: # value = photo
image_field = self.fields.get(extra)
image_field.save(value.name, value)
results[question][extra] = image_field.storage_path(value.name)
else:
results[question][extra] = value
self.instance.results = results
return super().save(commit)

Use a single function for multiple serializer fields with different arguments

I have a serializer for a model with an image field, for which I have saved multiple different sized thumbnail images.
I access them by returning their URL using the SerializerMethodField:
class GalleryImageSerializer(serializers.ModelSerializer):
image_sm = serializers.SerializerMethodField()
image_md = serializers.SerializerMethodField()
image_lg = serializers.SerializerMethodField()
image_compressed = serializers.SerializerMethodField()
def get_image_sm(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/sm.jpg')
def get_image_md(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/md.jpg')
def get_image_lg(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/lg.jpg')
def get_image_compressed(self, obj):
return default_storage.url(f'{splitext(obj.image.name)[0]}/compressed.jpg')
This code works, but it kind of violates the "don't repeat yourself" guideline.
As you can see, these are all duplicate SerializerMethodFields, with the only difference being the filename, eg 'lg.jpg', 'md.jpg', etc.
I'd much prefer to have only one function that I call with an argument for the filename, as an example(pseudocode):
class GalleryImageSerializer(serializers.ModelSerializer):
image_sm = serializers.SerializerMethodField(filename='sm.jpg')
image_md = serializers.SerializerMethodField(filename='md.jpg')
image_lg = serializers.SerializerMethodField(filename='lg.jpg')
image_compressed = serializers.SerializerMethodField(filename='compressed.jpg')
def get_image(self, obj, filename=''):
return default_storage.url(f'{splitext(obj.image.name)[0]}/{filename}')
Currently I am unable to find any way to achieve this. Reading the source code of SerializerMethodField, it doesn't seem to support it.
Is there any way to avoid creating duplicate functions for fields with arbitrary differences?
You can add these fields in the to_representation method.
def to_representation(self, instance):
ret = super().to_representation(instance)
# add img urls to ret dict
for name in ['sm', 'md', 'lg', 'compressed']:
ret['image_' + name] = default_storage.url(f'{splitext(instance.image.name)[0]}/{name}.jpg')
return ret
check the docs for more details:
https://www.django-rest-framework.org/api-guide/serializers/#to_representationself-instance

ModelForm saving over model data with empty fields

I'm building an Edit form for a model in my database using a ModelForm in Django. Each field in the form is optional as the user may want to only edit one field.
The problem I am having is that when I call save() in the view, any empty fields are being saved over the instance's original values (e.g. if I only enter a new first_name, the last_name and ecf_code fields will save an empty string in the corresponding instance.)
The form:
class EditPlayerForm(forms.ModelForm):
class Meta:
model = Player
fields = ['first_name', 'last_name', 'ecf_code']
def __init__(self, *args, **kwargs):
super(EditPlayerForm, self).__init__(*args, **kwargs)
self.fields['first_name'].required = False
self.fields['last_name'].required = False
self.fields['ecf_code'].required = False
The view:
def view(request, player_pk = ''):
edit_player_form = forms.EditPlayerForm(auto_id="edit_%s")
if "edit_player_form" in request.POST:
if not player_pk:
messages.error(request, "No player pk given.")
else:
try:
selected_player = Player.objects.get(pk = player_pk)
except Player.DoesNotExist:
messages.error(request, "The selected player could not be found in the database.")
return redirect("players:management")
else:
edit_player_form = forms.EditPlayerForm(
request.POST,
instance = selected_player
)
if edit_player_form.is_valid():
player = edit_player_form.save()
messages.success(request, "The changes were made successfully.")
return redirect("players:management")
else:
form_errors.convert_form_errors_to_messages(edit_player_form, request)
return render(
request,
"players/playerManagement.html",
{
"edit_player_form": edit_player_form,
"players": Player.objects.all(),
}
)
I've tried overriding the save() method of the form to explicitly check which fields have values in the POST request but that didn't seem to make any difference either.
Attempt at overriding the save method:
def save(self, commit = True):
# Tried this way to get instance as well
# instance = super(EditPlayerForm, self).save(commit = False)
self.cleaned_data = dict([ (k,v) for k,v in self.cleaned_data.items() if v != "" ])
try:
self.instance.first_name = self.cleaned_data["first_name"]
except KeyError:
pass
try:
self.instance.last_name = self.cleaned_data["last_name"]
except KeyError:
pass
try:
self.instance.ecf_code = self.cleaned_data["ecf_code"]
except KeyError:
pass
if commit:
self.instance.save()
return self.instance
I also do not have any default values for the Player model as the docs say the ModeForm will use these for values absent in the form submission.
EDIT:
Here is the whole EditPlayerForm:
class EditPlayerForm(forms.ModelForm):
class Meta:
model = Player
fields = ['first_name', 'last_name', 'ecf_code']
def __init__(self, *args, **kwargs):
super(EditPlayerForm, self).__init__(*args, **kwargs)
self.fields['first_name'].required = False
self.fields['last_name'].required = False
self.fields['ecf_code'].required = False
def save(self, commit = True):
# If I print instance variables here they've already
# been updated with the form values
self.cleaned_data = [ k for k,v in self.cleaned_data.items() if v ]
self.instance.save(update_fields = self.cleaned_data)
if commit:
self.instance.save()
return self.instance
EDIT:
Ok so here is the solution, I figured I'd put it here as it might be useful to other people (I've certainly learned a bit from this).
So it turns out that the is_valid() method of the model form actually makes the changes to the instance you pass into the form, ready for the save() method to save them. So in order to fix this problem, I extended the clean() method of the form:
def clean(self):
if not self.cleaned_data.get("first_name"):
self.cleaned_data["first_name"] = self.instance.first_name
if not self.cleaned_data.get("last_name"):
self.cleaned_data["last_name"] = self.instance.last_name
if not self.cleaned_data.get("ecf_code"):
self.cleaned_data["ecf_code"] = self.instance.ecf_code
This basically just checks to see if the fields are empty and if a field is empty, fill it with the existing value from the given instance. clean() gets called before the instance variables are set with the new form values, so this way, any empty fields were actually filled with the corresponding existing instance data.
You could maybe use the update() method instead of save()
or the argument update_field
self.instance.save(update_fields=['fields_to_update'])
by building the list ['fields_to_update'] only with the not empty values.
It should even work with the comprehension you've tried :
self.cleaned_data = [ k for k,v in self.cleaned_data.items() if v ]
self.instance.save(update_fields=self.cleaned_data)
EDIT :
Without overriding the save method (and commenting out this attempt in the form):
not_empty_data = [ k for k,v in edit_player_form.cleaned_data.items() if v ]
print(not_empty_data)
player = edit_player_form.save(update_fields=not_empty_data)
You could check the values if it's not empty in your view without overriding save()
if edit_player_form.is_valid():
if edit_player_form.cleaned_data["first_name"]:
selected_player.first_name = edit_player_form.cleaned_data["first_name"]
if edit_player_form.cleaned_data["last_name"]:
selected_player.last_name= edit_player_form.cleaned_data["last_name"]
if edit_player_form.cleaned_data["ecf_code"]:
selected_player.ecf_code= edit_player_form.cleaned_data["ecf_code"]
selected_player.save()
This should work fine with what you want. I'm not sure if it's the best way to do it but it should work fine.

No reference id found for related field odoo 8

I am trying to relate many2one field to another many2one field but return error.
I used on create method didn't work, I tried
jounral = field.Many2one('erp.journal', related="journal_item.journal
worked but on upgrade of database I receive error
There is no reference field journal_id found of journal.entries.
Here is my code
class JournalItem(models.Model):
_name = 'journal.item'
name = fields.Char()
journal = fields.Many2one('erp.journal')
entries = fields.One2many('journal.entries', 'journal_item')
class JournalEntries(models.Model):
_name = 'journal.entries'
#record Created but didn't work
#api.model
def create(self, vals):
vals['journal_id'] = self.journal_item.journal
return super(JournalEntries, self).create(vals)
#Error NoneType object has no attribute 'id'
#api.model
def create(self, vals):
for x in self:
x.journal_id = x.journal_item.journal
return super(JournalEntries, self).create(vals)
journal_item = fields.Many2one('journal.item')
journal_id = fields.Many2one('erp.journal')
Very first you need to understand the #api decorators usage, which decorators should be implemented when.
#api.model does not have any list of recordset, so you can not loop through it.
You can easily achieve this by defining relational field.
class JournalEntries(models.Model):
_name = 'journal.entries'
journal_item = fields.Many2one('journal.item')
journal_id = fields.Many2one(comodel_name='erp.journal', related='journal_item.journal', string='Journal', readonly=True)
Click here to read about #api decorators

How to make Django run a command only once per model

We're using django-MPTT for tree items and I have to create functionality for automatic email notifications when a tree item has been modified. Absolutely straight-forward using save signals but - if a parent item is modified, then n (n being number of childen items + parent item) emails are sent instead of just one (because parent's changes are automatically done to children). Is there a good way to prevent that and basically tell Django "Please send only one email per model"?
The best I've come up with is a kind of a hacky way (and a pretty bad hack): add a model to the database that is updated when the parent item is modified and with childrens always check that model before sending email. I.e. if HiddenModel exists, do not send email, else send email. And after like five seconds (probably in another thread) remove/undo modifications to that HiddenModel object. It would probably work, but performance-wise it's just bad, with all those database queries.
EDIT: Model: (shortened version for SO, probably has some errors in this form)
from mptt.models import MPTTModel, TreeForeignKey
class TreeItem(MPTTModel):
parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
users = models.ManyToManyField('accounts.User', blank=True)
name = models.CharField(max_length=255)
#property
def tree_path(self):
if self.is_root_node():
return []
tree = self.get_ancestors(include_self=False, ascending=False)
return list(map(lambda item: item.name, tree))
def save(self, *args, **kwargs):
is_new = self.pk is None
if not is_new:
prev_allowed_users = TreeItem.objects.get(pk=self.pk).users.all()
super().save(*args, **kwargs)
new_allowed_users = self.users.all()
if is_new:
self.send_access_email(list(new_allowed_users)) #TODO
else:
if prev_allowed_users != new_allowed_users:
send_to = self.get_new_access_users(prev_allowed_users, new_allowed_users) # TODO
self.send_access_email(send_to)
def get_new_access_users(self, prev_users, new_users):
send_to = []
for user in new_users:
if user not in prev_users:
send_to.append(user)
return send_to
def send_access_email(self, recipient_list):
email_content = 'example'
email = EmailMessage(
'subject',
email_content,
'ex#ample.com',
recipient_list)
print("Sending email to %s users" % str(len(recipient_list)))
You need some sort of registry to figure out if your object is being saved externally or as part of a tree traversing cascade. It seems that using a class variable might be a more elegant way of doing this. Here is some pseudo-code to illustrate what I mean.
class TreeItem(MPTTModel):
_save_registry = set()
... your model fields go here ...
def is_save_locked(self):
return any(p.id in TreeItem._save_registry for p in self.get_parents())
def save_lock(self):
TreeItem._save_registry.add(self.id)
def save_release(self):
TreeItem._save_registry.remove(self.id)
def save(self, *args, **kwargs):
if not self.is_save_locked:
self.save_lock()
... do things only original save should do ...
self.save_release()
... do things every save should do ...
return super(TreeItem, self).save(*args, **kwargs)

Categories

Resources