django admin dynamically set changeform initial data - django

I'm implementing a m2m relationship through an intermediate model using the default m2m widget. I have the Person and Project models related using the Membership model.
So far I've succeeded at displaying the default m2m widget in the Person change form and creating the intermediate model instances correctly, but my problem is populating the widget when a Person is being modified.
This is the form class I'm using with the PersonAdmin:
class PersonForm(forms.ModelForm):
projects = forms.ModelMultipleChoiceField(models.Project.objects.all(),
widget=widgets.FilteredSelectMultiple(
verbose_name="Projects",
is_stacked=False,
attrs={'rows':'10'}))
projects.required = False
class Meta:
model = models.Person
fields = ['name', 'last_name', 'personal_id_number',
'personal_id_type', 'administrative_viability',
'observations']
def save(self, commit=True):
ret = super(PersonForm, self).save(commit)
for p in self.cleaned_data['projects']:
models.Membership.objects.create(person=self.instance, project=p)
return ret
And the PersonAdmin itself:
class PersonAdmin(admin.ModelAdmin):
form = PersonForm
def get_changeform_initial_data(self, request):
initial = super(PersonAdmin, self).get_changeform_initial_data(request)
initial['projects'] = models.Person.get(pk=initial['person']).project_set.all()
return initial
I tried setting the initial value of projects in the method get_changeform_initial_data like that, but it doesn't work. Overall it looks like it's being ignored, as if I'm not overriding it properly.
Any help will be greatly appreciated!

This question gave me the idea of overriding the __init__ method of my PersonForm:
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
person = kwargs['instance']
initial = {'projects': person.project_set.all()}
kwargs['initial'] = initial
super(PersonForm, self).__init__(*args, **kwargs)
I still don't know why overriding get_changeform_initial_data wasn't working.

get_changeform_initial_data is only called if it's not a change. I know this makes no sense. I suspect it's a bug.
See django/contrib/admin/options.py from line 1573, which is the only call to this method in the whole of Django:
if add:
initial = self.get_changeform_initial_data(request)
form = ModelForm(initial=initial)
formsets, inline_instances = self._create_formsets(request, form.instance, change=False)
else:
form = ModelForm(instance=obj)
formsets, inline_instances = self._create_formsets(request, obj, change=True)
Update: Looks like it's deliberate. I'll ask the developers why it works like this.

Related

Django ChoiceField initial setup not working

I have ModelForm where i use Django Forms.ChoiceField. Writing the value to the database works. But when i open the url, the dropdown list is not showing the previously selected value as selected value.
I tried setting initial=value, but it's not working as well.
class GameForm(forms.ModelForm):
gameCode = forms.ChoiceField(required=False)
def __init__(self, *args, **kwargs):
obj = AllGameCodes.objects.filter(game=game)
choices = []
choices.append(('', '-----------'))
for i in obj:
choices.append((i.code,i.description))
self.fields['gameCode'].choices = choices
in views.py,
game = games.objects.get(id=1)
form = GameForm(request.POST, initial={'code':game.code}
You must take game variable from kwargs. Also using ModelChoicefield may ease your solution
def __init__(self, *args, **kwargs):
super(GameForm, self).__init__(*args, **kwargs)
_game = kwargs.get("game"):
if _game:
self.fields['gameCode'] = ModelChoiceField(queryset=AllGameCodes.objects.filter(game=_game), required=False)
For future reference, you may use form = GameForm(instance=game) to load the form with the model data and write new data to that model.
Also instead of overwriting the form class, you can alter fields in your view
#views.py
game = games.objects.get(id=1)
form = GameForm(request.POST, instance=game)
form.fields['gameCode'].queryset = AllGameCodes.objects.filter(game=game)

ModelChoiceField in forms.Form won't validate if queryset is overridden

I have a django ModelChoiceField that won't validate if I override the queryset.
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
form = PersonalNote()
form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
The form.is_valid() error is: "Select a valid choice. That choice is not one of the available choices".
If Tile.objects.none() is replaced with Tile.objects.all() it validates, but loads far too much data from the database. I've also tried:
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
def __init__(self, *args, **kwargs):
yyy = kwargs.pop('yyy', None)
super(PersonalNote, self).__init__(*args, **kwargs)
if yyy:
self.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
What might be wrong here? Note the real application also overrides the label, but that does not seem to be a factor here:
class ModelChoiceField2(forms.ModelChoiceField):
def label_from_instance(self, obj):
assert isinstance(obj,Tile)
return obj.child_title()
After 2 hours I found the solution. Because you specified a queryset of none in the class definition, when you instantiate that PersonalNote(request.POST) to be validated it is referenceing a null query set
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
To fix this, when you create your form based on a POST request be sure to overwrite your queryset AGAIN before you check is_valid()
def some_view_def(request):
form = PersonalNote(request.POST)
**form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)**
if form.is_valid():
#Do whatever it is
When you pass an empty queryset to ModelChoiceField you're saying that nothing will be valid for that field. Perhaps you could filter the queryset so there aren't too many options.
I also had this problem. The idea is to dynamically change the queryset of a ModelChoiceField based on a condition (in my case it was a filter made by another ModelChoiceField).
So, having the next model as example:
class FilterModel(models.Model):
name = models.CharField()
class FooModel(models.Model):
filter_field = models.ForeignKey(FilterModel)
name = models.CharField()
class MyModel(models.Model):
foo_field = models.ForeignKey(FooModel)
As you can see, MyModel has a foreign key with FooModel, but not with FilterModel. So, in order to filter the FooModel options, I added a new ModelChoiceField on my form:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
# your code here
self.fields['my_filter_field'] = forms.ModelChoiceField(FilterModel, initial=my_filter_field_selected)
self.fields['my_filter_field'].queryset = FilterModel.objects.all()
Then, on your Front-End you can use Ajax to load the options of foo_field, based on the selected value of my_filter_field. At this point everyting should be working. But, when the form is loaded, it will bring all the posible options from FooModel. To avoid this, you need to dynamically change the queryset of foo_field.
On my form view, I passed a new argument to MyForm:
id_filter_field = request.POST.get('my_filter_field', None)
form = MyForm(data=request.POST, id_filter_field=id_filter_field)
Now, you can use that argument on MyForm to change the queryset:
class MyForm(forms.ModelForm):
# your code here
def __init__(self, *args, **kwargs):
self.id_filter_field = kwargs.pop('id_filter_field', None)
# your code here
if self.id_filter_field:
self.fields['foo_field'].queryset = FooModel.objects.filter(filter_field_id=self.id_filter_field)
else:
self.fields['foo_field'].queryset = FooModel.objects.none()

How to display a custom field only in change view (admin)

I'm totally new in Python and Django :) and i need some help.
What i want to do:
I have a model Page and i need to add a custom field "message" when someone try to update one object.
Why? Because i'm building a revision system. This field, it's just an explanation about the change. So this field is not linked to the Page (but to another model PageRevision)
After some research i managed to add this field to my form in the admin.py file, like this:
class PageAdminForm(forms.ModelForm):
# custom field not backed by database
message = forms.CharField(required=False)
class Meta:
model = Page
it's working, my field is now displayed...But i don't want this field everywhere. Just when someone try to update a Page object.
i have found this answer different-fields-for-add-and-change-pages-in-admin but it's not working for me because it's a custom field (i think).
The rest of my code in admin.py:
class PageAdmin(admin.ModelAdmin):
form = PageAdminForm
fields = ["title", "weight", "description", "message"]
list_display = ["title", "weight", "description"]
list_filter = ["updated_at"]
def get_form(self, request, obj=None, **kwargs):
if obj is None:
# not working ?
kwargs['exclude'] = ['message']
# else:
# kwargs['exclude'] = ['message']
return super(PageAdmin, self).get_form(request, obj, **kwargs)
def save_model(self, request, obj, form, change):
if not obj.id:
obj.author = request.user
obj.modified_by = request.user
wiki_page = obj.save()
# save page in revision table
revision = PageRevision(change=change, obj=wiki_page,
request=request)
# retrieve value in the custom field
revision.message = form.cleaned_data['message']
revision.save()
def get_form doesn't exclude my custom message field because i think it doesn't know is existence. If i put another field like title, it's works.
So how to exclude the custom field from add view ?
Thanks :)
You're right, it won't work this way, because 'message' is not a field found on the Page model and the ModelAdmin class will ignore the exclusion. You can achieve this in many ways, but I think the best way to do it is this:
class PageAdmin(admin.ModelAmin):
change_form = PageAdminForm
...
def get_form(self, request, obj=None, **kwargs):
if obj is not None:
kwargs['form'] = self.change_form
return super(UserAdmin, self).get_form(request, obj, **defaults)
Basicaly here django will use an auto-generated ModelForm when adding a Page and your custom form when editing the Page. Django itself uses a similar technique to display different forms when adding and changing a User:
https://github.com/django/django/blob/stable/1.6.x/django/contrib/auth/admin.py (the interesting part is in line 68)
I just stumbled upon this same question and, since it comes in the first search results I would like to add this other solution for people that do not want to use a custom form.
You can override the get_fields method of your admin class to remove a custom field from the Add page.
def get_fields(self, request, obj=None):
fields = list(super().get_fields(request, obj=obj))
if obj is None:
fields.remove("message")
return fields

Passing arguments to ModelForm through FormSet

I'm kinda new to Formsets and I'm stuck at a problem.
I use a Modelform to allow the creation of a new object.
class AddUpdateEntryForm(forms.ModelForm):
class Meta:
model = Zeit
exclude = ('mitarbeiter', 'user_updated')
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(AddUpdateEntryForm, self).__init__(*args, **kwargs)
self.fields['projekt'].queryset = Projekt.objects.filter(firma=Mitarbeiter.objects.get(user_id=self.user).firma_id)
That form gets it's arguments from the view:
form = AddUpdateEntryForm(user=entry_user, initial=initial)
Now, I want to display multiple instances of that form on a single page.
I use:
forms.py:
AddEntryFormSet = formset_factory(form=AddUpdateEntryForm)
and
views.py:
formset = AddEntryFormSet(initial=initial)
which works fine, but only when I comment out the "self.user...." and "self.fields...." lines from ModelForm Class.
I tried several ways of passing the argument from the call inside the view to the ModelForm.
Is there a proper way to do this?
Thanks in advance
Conrad
It should be possible to subclass BaseModelFormset so that the user is passed to each form when it is constructed. However, that's quite tricky.
A simpler technique is to define a function that creates a model form for a given user, and dynamically create the model form class in the view.
def create_form(user):
"""Returns a new model form which uses the correct queryset for user"""
class AddUpdateEntryForm(forms.ModelForm):
class Meta:
model = Zeit
exclude = ('mitarbeiter', 'user_updated')
def __init__(self, *args, **kwargs):
super(AddUpdateEntryForm, self).__init__(*args, **kwargs)
self.fields['projekt'].queryset = Projekt.objects.filter(firma=Mitarbeiter.objects.get(user_id=user).firma_id)
return AddUpdateEntryForm
The closure of user in the function means that you can set the queryset correctly. Note that the __init__ method takes the same arguments as its parent class, so we no longer have any problems when we use modelformset_factory in the view.
AddUpdateEntryForm = create_form(user)
AddEntryFormSet = modelformset_factory(model=Zeit, form=AddUpdateEntryForm)

Django - ForeignKey field initial value definition in Admin

I have a Person model, which has a ForeignKey field to itself, called mother.
When the user goes to the 'add' admin form, I want to define an initial value for mother, in case there is a GET('mother') parameter, or leave it blank, in case there is not.
I have actually 2 questions:
How to access request inside ModelAdmin?
How to define initial value for a ForeignKey field?
In models.py:
class Person(models.Model):
name=models.CharField()
mother=models.ForeignKey('self')
In admin.py:
class PersonAdminForm(forms.ModelForm):
class Meta:
model = Person
class PersonAdmin(admin.ModelAdmin):
mother = request.GET.get('mother','') #don`t know how to access request
if mother != '':
form = PersonAdminForm
form.initial={'mother':Person.objects.get(id=mother)}
Well, this ain't working. Even if I only try to define a hardcoded initial value, it doesn`t work.
What am I doing wrong?
PS.: Of course, I may be asking the wrong questions, so I appreciate any help that solves the problem.
My solution:
class PersonAdmin(admin.ModelAdmin):
form = PersonAdminForm
# ...
def get_form(self, request, obj=None, *args, **kwargs):
form = super(PersonAdmin, self).get_form(request, *args, **kwargs)
# Initial values
form.base_fields['mother'].initial = None
if obj and obj.mother:
form.base_fields['mother'].initial = obj.mother
return form
Oh, it happens to be a lot easier than I thought.
If you pass a GET parameter with the name of the field as key to a Django`s add form, the GET parameters value will be set as initial value for that field.
In my case, I just needed to redirect to
localhost/admin/my_app/person/add/?&mother=< id >
There was no need for manipulating admin or anything.
Try overriding the get_form() method on ModelAdmin:
class PersonAdmin(admin.ModelAdmin):
form = PersonAdminForm
def get_form(self, request, *args, **kwargs):
form = super(PersonAdmin, self).get_form(request, *args, **kwargs)
mother = request.GET.get('mother', None)
if mother:
form.initial = {'mother': Person.objects.get(id=mother)}
return form