I am updating a django app from 1.4 to 1.8 and have hit a small problem with django admin.
My models look like this
def new_key():
return binascii.b2a_hex(os.urandom(20))
class ApiKey(models.Model):
customer = models.ForeignKey(UserProfile)
key = models.CharField(max_length=40, default=new_key)
And admin.py is
class ApiKeyInline(admin.StackedInline):
model = ApiKey
extra = 0
class UserProfileAdmin(admin.ModelAdmin):
inlines = [ApiKeyInline]
admin.site.register(UserProfile, UserProfileAdmin)
When using admin page api key get correctly populated with a random value. However when saving the UserProfile it doesn't get saved, it is as if nothing was added. If I manually change a a single leter in the autogenerated key saving works correctly. It seems to me this is a problem with django detecting a change or something like this.
Any suggestions? Code worked in 1.4.
There is a solution in one of the old threads: How to force-save an "empty"/unchanged django admin inline?
You should mark your inline form as always changed.
from django.forms import ModelForm
from .models import UserProfile
class AlwaysChangedModelForm(ModelForm):
def has_changed(self):
""" Should returns True if data differs from initial.
By always returning true even unchanged inlines will get validated and saved."""
return True
#An inline model
class ApiKey(admin.StackedInline):
model = ApiKey
extra = 0
form = AlwaysChangedModelForm
A short investigation lead me to the save_new_objects of class BaseModelFormSet, located in django/forms/models.py.
It has the following check: if not form.has_changed()
Looks promising, huh? Now, we want to "enhance" this method. Where to start? Well... Inlines inherit from InlineModelAdmin, which has an get_formset method. So...
class ApiKeyInline(admin.StackedInline):
model = ApiKey
extra = 0
def get_formset(self, *args, **kwargs):
formset = super(EmailInlineAdmin, self).get_formset(*args, **kwargs)
# at this point, formset is a generated class called like "ApiKeyFormSet"
# as it is a regular python objects, no one stops us from playing with it
formset.save_new_objects = ApiKeyInline.my_own_save_new_objects_method
return formset
def my_own_save_new_objects_method(self, commit=True):
# here should be something like
# django/forms/models.py BaseModelFormSet.save_new_objects
return self.new_objects
The contents of my_own_save_new_objects_method you have to edit yourself. The original method, once again, is in dist-packages/django/forms/models.py , either call it via super or write something entirely yours, anyways skip the check.
Also, maybe it is a terrible and overcomplicated solution. I have a feeling that there should be a better one. In example, not setting default=new_key in your model, but setting a default value for the form field, in example.
Related
Thanks in advance for reading this. I can't wrap my head around it and it's getting quite frustrating by now.
We have the following registration form:
class RegistrationForm(forms.ModelForm):
class Meta:
model = Register
fields = ('name', 'company_name')
def clean(self):
if is not self.cleaned_data.get('card').is_available():
raise forms.ValidationError(_('Error'))
The Register model includes a card linked to a Card model. This includes is_available() which functionally works.
Our flow is:
The end user selects the card which lists all registrations for it.
They click the 'Add registration'-button which brings them to cards/{PK}/add.
The Add registration-button is a generic.View. In post(self, request, pk) I have the following code:
form = RegistrationForm(request.POST)
But how do I pass it the contents of Card.objects.get(pk=pk) to it?
I tried:
data = request.POST.copy()
data['card'] = pk
form = RegistrationForm(data)
But I think because card is not included in fields it gets lost somewhere, which makes sense from a sanitize-all-input-point of view, but I would very much like to add the card dynamically, in this case.
Any ideas?
So, just use CreateView and study how it does things using the linked site.
There is no need to use generic.View as it's the basic of basics. You only want to implement all this logic using generic.View to get more familiar with the way things work or if you need some very special form handling.
The short version would be:
from django.views import generic
from myapp.forms import RegistrationForm
class CardCreateView(generic.CreateView):
form_class = RegistrationForm
ModelForm has a save method. The correct way to solve this is to use it with commit=False, that will return an object that hasn’t yet been saved to the database. Then you can alter that object before finally saving it.
This is explained here in the docs
So this is what your code should look like:
form = RegistrationForm(request.POST)
form.save(commit=False)
form.card = Card.objects.get(pk=pk)
form.save_m2m()
save_m2m should be used if your model has many-to-many relationships with other models. In my case, it was a OneToOne, so I used save() instead.
If you use a CreateView instead of the generic View, the snippet above should go into your overridden form_valid method
I have a form base class which checks if the instance the form is updating has changed, and does not save if it has not changed.
this is in my custom model form, I override save:
class MyModelForm(models.ModelForm):
# .. more code here..
def save(self, commit=True):
if self.has_changed():
# Won't do anything if the instance did not changed
return self.instance
return super(MyModelForm, self).save(commit)
A LOT of my forms use this base class.
Now, one of my forms have a few fields which I set to disabled=True (django 1.9 +). So in one of my forms:
def __init__(self, *args, **kwargs):
## ..code
self.fields['address'].disabled = True
After a lot of debugging why the form.has_changed() is True (hence the instance is saved for no reason), even when I save the form without changing the instance. I've found out that django includes disabled fields in changed_data - which makes no sense, as disabled fields should not be altered by the user anyway.
Am I missing something or it is a bug, or maybe that how it should work?
How can I resolve this without too much changes, as the form base class is used a lot in my code.
This is a known issue with DjangoProject with the ticket at https://code.djangoproject.com/ticket/27431 and the corresponding PR at https://github.com/django/django/pull/7502. As this answer is being written the PR is merged with master so the latest version should have this fixed.
A workaround this is as follows
for form in formset:
if form.has_changed() and form not in formset.deleted_forms:
fields = form.changed_data
up_f = [field for field in fields if not form.fields[field].disabled]
if len(up_f) > 0:
updated_data.append(form.cleaned_data)
This results in updated_data having the only forms that are updated and not deleted.
I have two basic models that use model forms in the Django admin.
Models.py is similar to:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField()
class OtherModel(models.Model):
model = models.ForeignKey(FirstModel)
##Other fields that show up fine and save fine, but include some localflavor
Forms.py looks similar to:
class FirstModelForm(forms.ModelForm):
def clean(self):
#call the super as per django docs
cleaned_data = super(FirstModelForm, self).clean()
print cleaned_data
class Meta:
model = FirstModel
#other modelform is the same with the appropriate word substitutions and one field that gets overridden to a USZipCodeField
These are a stacked inline ModelAdmin with nothing special in the admin.py:
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (#my list of fields works correctly)
readonly_fields = (#couple read onlys that work correctly)
class FirstModelAdmin(admin.ModelAdmin):
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
I do have a User model, form and ModelAdmin that subclasses the User and UserCreationForm and overrides it's own clean method.This works exactly as expected.
The problem is with FirstModel and OtherModel. The clean methods I override in the ModelForm subclasses of FirstModelForm and OtherModelForm don't do anything. No exception thrown or a print of the cleaned_data. Just nothing. Everything else works as expected, but it's like my clean method isn't even there.
I got to be missing something simple, but I can't see what is. Any help would be great. Thanks!
By default, Django dynamically generates a model form for your model admins. You must specify that you want to use your custom forms by setting the form attribute.
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (...) # if this doesn't work after specifying the form, set fields for the model form instead
readonly_fields = (#couple read onlys that work correctly)
form = OtherModelForm
class FirstModelAdmin(admin.ModelAdmin):
form = FirstModelForm
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
You need to return the cleaned_data from the clean method in the form. If you look at the documentation for cleaning fields that rely on each other you'll notice:
...
# Always return the full collection of cleaned data.
return cleaned_data
It is possible that nothing survived the parent 'clean' method. If you are submitting data that won't validate because of the way your models are set up, cleaned_data will be empty. This is mentioned in the same doc linked by Timmy, where it says:
By the time the form’s clean() method is called, all the individual field clean methods will have been run (the previous two sections), so self.cleaned_data will be populated with any data that has survived so far. So you also need to remember to allow for the fact that the fields you are wanting to validate might not have survived the initial individual field checks.
In this case, if you have a URLField, the field validation is very strict, and unless you define 'verify_exists=False', it will also check if you are putting in a URL that returns a 404. In your case you would need to do this if you wanted to allow that:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField(verify_exists=False)
Outside of that, I have no idea what could be going on.
In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})
After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.
I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet
The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)
You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.
Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.
I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())
I have a fairly simple Django application (v1.3 on Red Hat) for which I'm using the admin application to create and modify database records. One of the fields in my underlying model is a date field. Each time the corresponding field is displayed in the admin's new or edit form I'd like the initial value of this field to be today's date (and time). The user may choose to modify it thereafter, if she desires.
I know that I can set the default field value within my model definition (i.e. in models.py). Which works fine when a database record is first created. But for subsequent invocations of the change form the callable that I've assigned to the default parameter (datetime.datetime.now) obviously doesn't get invoked.
I've looked at - and tried - pretty well all of the many proposed solutions described elsewhere in stackoverflow, without success. Most of these appear to revolve around inserting initialisation code into the ModelForm subclass, e.g. either something like this...
class ConstantDefAdminForm(ModelForm) :
a_date_field = DateField(initial="datetime.datetime.now") # or now()
class Meta :
model = ConstantDef
widgets = {
...
}
or something like this...
class ConstantDefAdminForm(ModelForm) :
class Meta :
model = ConstantDef
widgets = {
...
}
def __init__(self, ...) :
# some initialisation of a_date_field
super(ConstantDefAdminForm, self).__init__(...)
But neither of these approaches work. The initial field value is always set to the value that is stored in the database. My reading of the Django documentation is that the various ways of imposing initial field values in forms only work for unbound forms, not bound forms. Right?
But this capability (to selectively override currently stored values) would seem to be such a popular requirement that I'm convinced that there must be a way to do it.
Has anyone out there succeeded in doing this?
Thanks in advance,
Phil
In Django 1.4 the default=<callable> in model's declaration works well:
class MyModel(models.Model):
dt = models.TimeField(null=True, blank=True, default=datetime.datetime.now)
every time you add a record the default value of the field is updated.
But the use the field's default parameter cause me some problem with the Admin log history of DateField objects, that are every time recorded as changed also when they are not modified. So I've adopted a solution based on https://stackoverflow.com/a/11145346/1838607:
import datetime
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
self.fields['dt'].initial = datetime.datetime.now
class MyModelAdmin(admin.ModelAdmin):
form = MyModelAdminForm
fields = ('dt',)
Here's an approach that might work. In your model admin class, change the value of obj.a_date_field before the form is bound. The 'default' value for the date field should be the new value.
class MyModelAdmin(ModelAdmin):
...
def get_object(self, request, object_id):
obj = super(MyModelAdmin, self).get_object(request, object_id)
if obj is not None:
obj.a_date_field = datetime.now()
return obj
Note that get_object is not documented, so this is a bit hacky.
I had a similar problem, and I found the solution from here
I think what you will want to do is this:
class yourAdminModel(admin.ModelAdmin):
fields = ['your_date_field']
def add_view(self, request, form_url="", extra_context=None):
data = request.GET.copy()
data['your_date_field'] = datetime.date.today() # or whatever u need
request.GET = data
return super(yourAdminModel, self).add_view(request, form_url="", extra_context=extra_context)
You should be able to use auto_now with your DateTime Field which according to the docs will automatically set the value to now() each time the form is saved
Since Django 1.7 there is a function get_changeform_initial_data in ModelAdmin that sets initial form values:
def get_changeform_initial_data(self, request):
return {'dt': datetime.now()}