I'm creating a generic save method for a form.
In the save I want to iterate over the field and if the field is an instance of FileField to to some operations
def save(self, commit=True):
obj = super().save(commit=False)
for field in self.fields:
if isinstance(field, forms.FileField) and obj:
This is not working because, if I check the type of field is str. How can I get the real model or declared type ?
I found the answer: the self.fields is an OrderedDictionary, so I was iteraing over the keys instead of iterating over the values:
for field,value in self.fields.items():
if isinstance(value, forms.FileField) and obj:
Related
Using a generic CreateView in Django I'm trying to save only the fields that have been changed by user.
I'm trying to do this in my view:
def form_valid(self, form):
if form.has_changed():
for field in form:
if field.name in form.changed_data:
continue
else:
form.instance.field=None
In the last line I got this error:
object has no attribute 'field'
Is there a way to dynamically access to each field in the form? so I could change it?
Is there a way to dynamically access to each field in the form.
Yes.
form.fields[field_name]
So I could change it?
Yes.
form.fields[field_name] = django.forms.Charfield(required=False)
But in your code, probably you want to achieve value of the field:
field_value = form[field_name].value
If you have a data bounded field, you can change also data of form:
form.data(form.add_prefix(field_name)) = new_value
But this is already wrong approach to change something here.
I can imagine: in your case you don't understand how form / modelform saves the data and therefore you want to do something unnecessary:
Once more time, how ModelForm "saves" the data in DB:
Form validate the data.
Form clean the data.
on form.save(**kwargs) - Form create the instance and:
call instance.save() if form.save(commit=True)
dont call instance.save() if form.save(commit=False)
Instance saves the data.
Form don't save anything itself, instance.save - There is all what you need.
instance.save(self, force_insert=False, force_update=False, using=None, update_fields=None)
In your case you use CreateView - there all fields should be initiated, you can not UPDATE something, this something not exists yet. You can only avoid empty values, if you want.
def form_valid(self, form):
if form.has_changed():
form.cleaned_data = {key:val for key,val in form.cleaned_data.items() if val and key in form.changed_data}
return super().form_valid(form)
al last: form.instance.field is ridiculous
form has fields. You can achieve every field by name.
Model class has fields. You can achieve every field by name.
instance has attributes or properties. You can achieve every attribute by name.
And right now: what you are really want to do?
With this method, I can make a field save as lowercase, but this does not
change the field in the existing model (that is in memory).
def get_prep_value(self, value):
value = super(LowercaseField, self).get_prep_value(value)
if value is not None:
value = value.lower()
return value
I'm having a hard time figuring out how to force this field to lowercase without overriding save and doing the change there. But that splits the logic for this lowercase field. I'd like all of it in the field. What do I override so that setting this value forces lowercase in memory AND on in the DB?
I don't want to change a form, I want all the lowercase logic contained inside the field class.
I've found a partial work around like so:
def pre_save(self, model_instance, add):
""" Returns field's value just before saving. """
attr = getattr(model_instance, self.attname)
if attr is not None:
attr = attr.lower()
setattr(model_instance, self.attname, attr)
return attr
def get_prep_value(self, value):
value = super(LowercaseField, self).get_prep_value(value)
if value is not None:
value = value.lower()
return value
It has a bit of a code smell, and does not handle checking the value before a save, but I don't see how to do that without overriding setattr on the actual model class and catching dealing with that inside the model class itself.
You can override the "save" method in Django by adding the following code in your models.py file
def save(self, *args, **kwargs):
self.yourfiled = self.yourfield.lower()
return super(ModelsName, self).save(*args, **kwargs)
Of course is possible to handle all params with a loop.
For all existing record you can create a Management command that can convert all strings to lowercase
here the docs:
Writing custom django-admin commands
If you don't want to change the Save method, just add to the form the "|lower" tag that will be convert all string to lowercase in UI
{{ value|lower }}
I'm running into a issue that I cannot retrieve the value of an instance before I save it. It is returning the updated value in the save() method. Also, I cannot store in __init__ because it is running too many queries. I want to run the queries only when the user saves that instance
Here are my models simplified:
class Users(models.Model):
username = models.CharField(max_length=20)
class Groups(models.Model):
name = models.CharField(max_length=100)
user = models.ManyToManyField(Users)
'''def __init__(self, *args, **kwargs):
super(Groups, self).__init__(*args, **kwargs)
#store original values
for field in Groups._meta.get_fields():
f = field
if not f.many_to_many:
setattr(self, '__original_%s' % f.name, getattr(self, f.name))
else:
userlist = Users.objects.all().filter(groups__id=self.pk)
setattr(self, '__original_userlist', set(userlist)) #set() to force lazy query and get original value'''
#function trying to reproduce __init__
def store(self):
for field in Groups._meta.get_fields():
f = field
if not f.many_to_many:
setattr(self, '__original_%s' % f.name, getattr(self, f.name))
else:
userlist = Users.objects.all().filter(groups__id=self.pk)
setattr(self, '__original_userlist', set(userlist)) #set() to force lazy query
def save(self, *args, **kwargs):
self.store()
print(getattr(self, '__original_userlist')) #return the list with users
super(Groups, self).save(*args, **kwargs)
print(getattr(self, '__original_userlist')) #return the list with users
I'm not sure the best way to approach since I have to make the queries. I'm doing that to compare the old and new value, then I can store the values which are different in a log file.
I solved this issue by querying the instance again before the super() call and using that object. It returned the old values for all the fields, but I need to check the m2m field as well and it was showing as users.None.
To get the values of the m2m I had to use the m2m_change signal!
I have a table of forms of the same class which contains ModelChoiceField. And each form in one row has the same queryset for this field. Problem is that every time the form is rendered, it is a new query which increases unbearably the number of queries.
The only solution I came up with is to construct the form on the go with js instead of letting django to render it itself. Is there a way to cache these querysets or somewhat preload it at once?
views.py:
shift_table=[]
for project in calendar_projects:
shift_table.append([])
project_branches = project.branches.all()
for i, week in enumerate(month):
for day in week:
shift_table[-1].append(
CreateShiftCalendarForm(initial={'date': day}, branch_choices=project_branches))
forms.py:
CreateShiftCalendarForm(EditShiftCalendarForm):
class Meta(ShiftForm.Meta):
fields = ('project_branch', 'date') + ShiftForm.Meta.fields
widgets = {'date': forms.HiddenInput(), 'length': forms.NumberInput(attrs={'step': 'any'}), 'project_branch': forms.Select()}
def __init__(self, *args, **kwargs):
branch_choices = kwargs.pop('branch_choices', ProjectBranch.objects.none())
super(CreateShiftCalendarForm, self).__init__(*args, **kwargs)
self.fields['project_branch'].queryset = branch_choices
self.fields['project_branch'].empty_label = None
ModelChoiceField is an subclass of ChoiceField in which "normal" choices are replaced with iterator that will iterate through provided queryset. Also there is customized 'to_python' method that will return actual object instead of it's pk. Unfortunately that iterator will reset queryset and hit database once again for each choice field, even if they are sharing queryset
What you need to do is subclass ChoiceField and mimic behaviour of ModelChoiceField with one difference: it will take static choices list instead of queryset. That choices list you will build in your view once for all fields (or forms).
A maybe less invasive hack, using an overload of Django's FormSets and keeping the base form untouched (i.e. keeping the ModelChoiceFields with their dynamic queryset):
from django import forms
class OptimFormSet( forms.BaseFormSet ):
"""
FormSet with minimized number of SQL queries for ModelChoiceFields
"""
def __init__( self, *args, modelchoicefields_qs=None, **kwargs ):
"""
Overload the ModelChoiceField querysets by a common queryset per
field, with dummy .all() and .iterator() methods to avoid multiple
queries when filling the (repeated) choices fields.
Parameters
----------
modelchoicefields_qs : dict
Dictionary of modelchoicefield querysets. If ``None``, the
modelchoicefields are identified internally
"""
# Init the formset
super( OptimFormSet, self ).__init__( *args, **kwargs )
if modelchoicefields_qs is None and len( self.forms ) > 0:
# Store querysets of modelchoicefields
modelchoicefields_qs = {}
first_form = self.forms[0]
for key in first_form.fields:
if isinstance( first_form.fields[key], forms.ModelChoiceField ):
modelchoicefields_qs[key] = first_form.fields[key].queryset
# Django calls .queryset.all() before iterating over the queried objects
# to render the select boxes. This clones the querysets and multiplies
# the queries for nothing.
# Hence, overload the querysets' .all() method to avoid cloning querysets
# in ModelChoiceField. Simply return the queryset itself with a lambda function.
# Django also calls .queryset.iterator() as an optimization which
# doesn't make sense for formsets. Hence, overload .iterator as well.
if modelchoicefields_qs:
for qs in modelchoicefields_qs.values():
qs.all = lambda local_qs=qs: local_qs # use a default value of qs to pass from late to immediate binding (so that the last qs is not used for all lambda's)
qs.iterator = qs.all
# Apply the common (non-cloning) querysets to all the forms
for form in self.forms:
for key in modelchoicefields_qs:
form.fields[key].queryset = modelchoicefields_qs[key]
In your view, you then call:
formset_class = forms.formset_factory( form=MyBaseForm, formset=OptimFormSet )
formset = formset_class()
And then render your template with the formset as described in Django's doc.
Note that on form validation, you will still have 1 query per ModelChoiceField instance, but limited to a single primary key value each time. That is also the case with the accepted answer. To avoid that, the to_python method should use the existing queryset, which would make the hack even hackier.
This works at least for Django 1.11.
I subclassed ChoiceField as suggested by GwynBleidD and it works sufficiently for now.
class ListModelChoiceField(forms.ChoiceField):
"""
special field using list instead of queryset as choices
"""
def __init__(self, model, *args, **kwargs):
self.model = model
super(ListModelChoiceField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value in self.empty_values:
return None
try:
value = self.model.objects.get(id=value)
except self.model.DoesNotExist:
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
if any(value.id == int(choice[0]) for choice in self.choices):
return True
return False
I am trying to edit django.contrib.auth.forms.UserChangeForm. Basically, auth_user's user edit page.
https://github.com/django/django/blob/master/django/contrib/auth/forms.py
According to source code, the form does not have a save() method, so it should inherit from forms.ModelForm right?
For full code, see here
class MyUserAdminForm(forms.ModelForm):
class Meta:
model = User
def __init__(self, *args, **kwargs):
super(MyUserAdminForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id: # username and user id
... the rest of the __init__ is setting readonly fields
.... some clean methods .....
def save(self, *args, **kwargs):
kwargs['commit'] = True
user = super(MyUserAdminForm, self).save(*args, **kwargs)
print user.username
print 'done'
return user
When I hit save, it said 'UserForm' object has no attribute 'save_m2m'. I've googled quite a bit, and tried to use add() but didn't work. What's causing this behaviour?
The thing is: the two print statements are printed. But the value never saved into database. I thought that the 2nd line would have saved once already.
Thanks
Remove the kwargs['commit'] = True line and see what happen.
Django Admin would invoke form.save_m2m(), which is hooked to the form when commit is False, here. The unconditional overriding of kwargs['commit'] = True would break the setattr of save_m2m() to form thus no attribute error is raised. The actual affected logic is here:
def save_form(self, request, form, change):
"""
Given a ModelForm return an unsaved instance. ``change`` is True if
the object is being changed, and False if it's being added.
"""
return form.save(commit=False)
You could find out that your version of form.save() overriding commit=False to commit=True unconditionally, thus Django Admin fails to continue as it believes form.save(commit=False) is invoked and thus form.save_m2m() needs to be called.
Refs the doc:
Another side effect of using commit=False is seen when your model has
a many-to-many relation with another model. If your model has a
many-to-many relation and you specify commit=False when you save a
form, Django cannot immediately save the form data for the
many-to-many relation. This is because it isn't possible to save
many-to-many data for an instance until the instance exists in the
database.
To work around this problem, every time you save a form using
commit=False, Django adds a save_m2m() method to your ModelForm
subclass. After you've manually saved the instance produced by the
form, you can invoke save_m2m() to save the many-to-many form data.