I'm using the ModelFormSetView class in django-extra-views to create a formset view of all WorkerStatus entries connected to a Worker. I'd also like to use custom validation on the formset, so I've defined my own formset_class and form_class in the view. Here's the view definition:
class WorkerStatusUpdateView(ModelFormSetView):
model = WorkerStatusEntry
formset_class = WorkerStatusFormSet
form_class = WorkerStatusForm
template_name = 'staff/workers/worker_status_update.tmpl'
can_delete = True
can_order = False
fields = ['status', 'start_date']
def dispatch(self, request, *args, **kwargs):
self.worker = Worker.objects.get(pk=self.kwargs['worker_pk'])
return super(WorkerStatusUpdateView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
ctx = super(WorkerStatusUpdateView, self).get_context_data(*args, **kwargs)
ctx['worker'] = self.worker
return ctx
def get_queryset(self, *args, **kwargs):
return self.worker.statuses.all()
...and here are the definitions of the form and formset respectively:
class WorkerStatusForm(forms.ModelForm):
class Meta:
model = WorkerStatusEntry
fields = ['status', 'start_date']
class WorkerStatusFormSet(BaseModelFormSet):
class Meta:
model = WorkerStatusEntry
def __init__(self, queryset, *args, **kwargs):
super(WorkerStatusFormSet, self).__init__(*args, **kwargs)
def clean(self):
print "Cleaning"
This results in a page where EVERY WorkerStatusEntry in the database is shown in the formset, regardless of get_queryset(). One thing you'll notice is that WorkerStatusFormSet.__init__ takes a queryset argument: I put that there because there was a queryset argument passed to it from the ModelFormSetView, but I don't know what to do with it.
Another thing to note: if I take formset_class = WorkerStatusFormSet out of the view definition, the correct queryset shows up in the formset. However I need to use my own formset class to validate across the whole formset. Unless there's another way?
The problem is your WorkerStatusFormSet.__init__ method. Looking at the code for BaseModelFormSet, the __init__ method already takes a queryset parameter. Since you aren't doing anything in your __init__ method except calling super(), the easiest fix is to remove it.
It's not a good idea to change the signature of the __init__ method as you have done for two reasons
def __init__(self, queryset, *args, **kwargs):
super(WorkerStatusFormSet, self).__init__(*args, **kwargs)
You have changed the order of the arguments. If you look at the code for BaseModelFormset, the first argument is data. That means that data might be incorrectly assigned to queryset if somebody calls WorkerStatusFormSet(data, ...)
You do not do anything with queryset or pass it to super(), so it is lost.
Related
I want to filter form fields querysets based on the user selected. Therefore, I want to pass user as argument to the form in order to filter fields querysets in the form's __init__ method. When I pass any arguments to the form I get the following error.
class UserDetailView(LoginRequiredMixin, FormMixin, DetailView):
model = TbUser
form_class = TbPeopleEntranceRightForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = TbPeopleEntranceRightForm(user=self.object)
context['form'] = form
return context
__init__() got an unexpected keyword argument 'user'
how do I pass the argument correctly, and how I get it in the __init__ form method?
Update:
class TbPeopleEntranceRightForm(forms.ModelForm):
def __init__(self, user, **kwargs):
super().__init__(**kwargs)
print(user)
# Use `self.user` here or in some other methods.
__init__() missing 1 required positional argument: 'user'
don't do it in get_context_data, it's made for something else.
Use get_form_kwargs instead:
class UserDetailView(LoginRequiredMixin, FormMixin, DetailView):
model = TbUser
form_class = TbPeopleEntranceRightForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.object
return kwargs
And in your forms.py:
def __init__(self, user=None, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
You need to add the parameter to the form's __init__ method:
class TbPeopleEntranceRightForm(forms.ModelForm):
...
def __init__(self, user=None, **kwargs):
super().__init__(**kwargs)
self.user = user
# Use `self.user` here or in some other methods.
Also, the correct way to then pass the user argument to the form is to override get_form_kwargs in the view, like #MojixCoder showed.
I have trouble setting up a form with a ModelMultipleChoiceField where the queryset depends on the user. My goal is to implement an export function.
My view looks like this:
class ExportView(FormView):
template_name = 'ExportTemplate.html'
def get(self, request, *args, **kwargs):
self.form_class = ExportForm(user = request.user)
return render(request, self.template_name, {'form': self.form_class})
def get_success_url(self):
return '/addrbook/'
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
return super().form_valid(form)
form:
class ExportForm(forms.Form):
def __init__(self, user, *args, **kwargs):
usersContacts = ContactManager().getAllUsersContacts()
self.contactList = forms.ModelMultipleChoiceField(queryset = usersContacts[str(user)])
print(usersContacts[str(user)])
super(ExportForm, self).__init__(*args, **kwargs)
I verified that the queryset is not empty, it contains a list of model objects.
My template looks like this:
<form method="post">{% csrf_token %}
{{ form }}
<input type="submit">
</form>
the only thing that gets rendered is the submit button.
Another thing that left me completely unsure of python basics is that this code:
class ExportForm(forms.Form):
contactList = forms.ModelMultipleChoiceField(queryset = [])
def __init__(self, user, *args, **kwargs):
usersContacts = ContactManager().getAllUsersContacts()
self.contactList.queryset = usersContacts[str(user)]
print(usersContacts[str(user)])
super(ExportForm, self).__init__(*args, **kwargs)
returned the runtime error:
'ExportForm' object has no attribute 'contactList'
How is it possible? the contactList member is part of the ExportForm class definition and 'self' should point to an object of that class.
Could someone explain to me why the form field is rendered empty and/or point me to a better way to pass the user to the form?
Edit: here are the changes i made to the answer to get it working, although i now stumbled upon a different problem(the field expects a queryset, and not a list of model objects):
View:
class ExportView(FormView):
template_name = 'ExportTemplate.html'
form_class = ExportForm
def get_form_kwargs(self):
kwargs = super(ExportView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
form:
class ExportForm(forms.Form):
contactList = forms.ModelMultipleChoiceField(queryset = Contact.objects.none())
def __init__(self, *args, **kwargs):
user = kwargs['user']
kwargs.pop('user', None)
super(ExportForm, self).__init__(*args, **kwargs)
usersContacts = ContactManager().getAllUsersContacts()
self.fields['contactList'].queryset = usersContacts[str(user)]
print(self.fields['contactList'].queryset)
First of all, you should pass the user to the form every time you instantiate it, not just in the get method. The way to do this with FormView is to override get_form_kwargs.
def get_form_kwargs(self):
kwargs = super(ExportForm, self).get_form_kwargs()
kwargs[user] = self.request.user
return kwargs
You can then remove your get() method.
Then, in your form class, you should use the none() method instead of an empty list to get an empty queryset. In the __init__ method you can pop the user from kwargs, and then call super() before you edit the fields. You edit the contactList field via self.fields instead of self.contactList. Note that the recommended style for field names in Django is contact_list instead of contactList.
class ExportForm(forms.Form):
contactList = forms.ModelMultipleChoiceField(queryset=YourModel.objects.none())
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(ExportForm, self).__init__(*args, **kwargs)
usersContacts = ContactManager().getAllUsersContacts()
self.fields['contactList'].queryset = usersContacts[str(user)]
You haven't shown the ContactManager() code, but using str(user) as the dictionary key looks fragile. It would probably be better to use user.pk or user.username instead.
Just going to put some code and explain at the bottom.
# modelforms.py
#
class MyModelModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(MyModelModelForm, self).__init__(*args, **kwargs)
print '__init__'
class Meta:
model = MyModel
exclude = ('my_fk', )
Here I am using django-vanilla-views
# views.py
#
class MyCreateView(NoAdminNoStaffLoginRequiredMixin, HasPermissionsMixin, CreateView):
template_name = 'modelform_create.html'
form_class = MyOtherModelModelForm # this modelform isn't important
required_permission = 'create_model'
def get_success_url(self):
return reverse_lazy('mymodel_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(MyCreateView, self).get_context_data(**kwargs)
# FIXME: form's __init__ being called a lot
MyModelFormset = modelformset_factory(
MyModel, form=MyModelModelForm, extra=4, max_num=10, validate_max=True)
if self.request.POST:
formset = MyModelFormset(
self.request.POST,
queryset=MyModel.objects.none(),
form_kwargs={'request': self.request})
else:
formset = MyModelFormset(
queryset=MyModel.objects.none(),
form_kwargs={'request': self.request})
print formset.total_form_count() # 4, which makes sense
context['formset'] = formset
return context
MyModelModelForm __init__ gets called 18 times. Yeah, I was wondering if I could get that from not happening.
Reason being I would like it to not be called multiple times is I query the DB for forms.ChoiceField(choices=...), which queries the DB that many times unnecessarily. Or if there is a better spot for populating choices (NOTE: I cannot just do it in the same space where the field is declared.) It is a mixin that populates this certain field.
If any more info is needed let me know. I had to trim down the code for only relevant info.
Im trying to build a FormView for an app that needs to be subclassed afterwards. Sadly I was not able to set the formclass by the subclass.
My Code:
class EventCreateView(FormView):
template_name='Events/add_event_form.html'
success_url = reverse_lazy('events_list')
form_class = None # replaced by __init__ function
def __init__(self, *args, **kwargs):
self.form_class=EventForm
return super(EventCreateView, self).__init__(*args, **kwargs)
#other functions, not shown here ..
class TrainingCreateView(EventCreateView):
def __init__(self, *args, **kwargs):
self.form_class=TrainingForm
return super(TrainingCreateView, self).__init__(*args, **kwargs)
urls.py:
urlpatterns = patterns('',
url(r'event/event/add/$', EventCreateView.as_view(), name='event_add'),
url(r'event/training/add/$', TrainingCreateView.as_view(), name='training_add'),
)
What am I doing wrong?
Try this instead:
class EventCreateView(FormView):
template_name='Events/add_event_form.html'
success_url = reverse_lazy('events_list')
form_class = EventForm
...
class TrainingCreateView(EventCreateView):
form_class = TrainingForm
This doesn't work for the TrainingCreateView because the __init__ view does the following
It sets self.form_class = TrainingForm
super(TrainingCreateView, self).__init__(*args, **kwargs) calls the __init__ of EventCreateView ...
Which sets self.formclass = EventForm
You can get around this by changing the order of your __init_ method. Note that the method doesn't have to return anything.
class TrainingCreateView(EventCreateView):
def __init__(self, *args, **kwargs):
super(TrainingCreateView, self).__init__(*args, **kwargs)
self.form_class = TrainingForm
However, from the code you've written, it is not clear why you need to set self.form_class in the __init__ method, rather than just setting it as a class attribute. If you need to set it dynamically, a better option might be to override get_form_class instead.
I have a model along with a ModelForm based on that model. The ModelForm contains a ModelMultipleChoice field, which I specify in the subclass of my ModelForm:
class TransactionForm(ModelForm):
class Meta:
model = Transaction
def __init__(self, *args, **kwargs):
super(TransactionForm, self).__init__(*args, **kwargs)
self.fields['category'] = forms.ModelChoiceField(queryset=Category.objects.filter(user=user))
As you can see, I need to filter the Category queryset by user. In other words, users should only see their own categories on the drop down. But how can I do this when user, or more specifically, request.user, is not available in a Model instance?
Edit: Adding my subclass of the CBV:
class TransUpdateView(UpdateView):
form_class = TransactionForm
model = Transaction
template_name = 'trans_form.html'
success_url='/view_trans/'
def get_context_data(self, **kwargs):
context = super(TransUpdateView, self).get_context_data(**kwargs)
context['action'] = 'update'
return context
I tried form_class = TransactionForm(user=request.user) and I'm getting a NameError saying that request was not found.
You can pass request.user to form init in view:
def some_view(request):
form = TransactionForm(user=request.user)
and add user parameter to form __init__ method (or pop it from kwargs in form):
class TransactionForm(ModelForm):
class Meta:
model = Transaction
# def __init__(self, *args, **kwargs):
# user = kwargs.pop('user', User.objects.get(pk_of_default_user))
def __init__(self, user, *args, **kwargs):
super(TransactionForm, self).__init__(*args, **kwargs)
self.fields['category'] = forms.ModelChoiceField(
queryset=Category.objects.filter(user=user))
update: in class based views you can add extra parameter to form init in get_form_kwargs:
class TransUpdateView(UpdateView):
#...
def get_form_kwargs(self):
kwargs = super(YourView, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs