Related
I'm using Django with the REST Framework. In a serializer, I would like to assign a field value based on a view or request (request.data['type']) parameter, so I need the view/request in the context.
I succeeded, but only in a cumbersome way, and I am looking into ways to simplify the code. Here's the successful approach (omitting irrelevant fields):
class TypeDefault(object):
def set_context(self, serializer_field):
view = serializer_field.context['view'] # or context['request']
self.type = view.kwargs['type'].upper()
def __call__(self):
return self.type
class RRsetSerializer(serializers.ModelSerializer):
type = serializers.CharField(read_only=True, default=serializers.CreateOnlyDefault(TypeDefault()))
class Meta:
model = RRset
fields = ('type',)
read_only_fields = ('type',)
To simplify things, I tried removing the TypeDefault class, and replacing the type serializer field by
type = serializers.SerializerMethodField()
def get_type(self, obj):
return self.context.get('view').kwargs['type'].upper() # also tried self._context
However, context.get('view') returns None. I am unsure why the view context is not available here. My impression is that it should be possible to get the desired functionality without resorting to an extra class.
As a bonus, it would be nice to specify the default in the field declaration itself, like
type = serializers.CharField(default=self.context.get('view').kwargs['type'].upper())
However, self is not defined here, and I'm not sure what the right approach would be.
Also, I am interested if there is any difference in retrieving information from the view or from the request data. While the context approach should work for both, maybe there's a simpler way to get the CreateOnlyDefault functionality when the value is obtained from request data, as the serializers deals with the request data anyways.
Edit: Per Geotob's request, here is the code of the view that calls the serializer:
class RRsetsDetail(generics.ListCreateAPIView):
serializer_class = RRsetSerializer
# permission_classes = ... # some permission constraints
def get_queryset(self):
name = self.kwargs['name']
type = self.kwargs.get('type')
# Note in the following that the RRset model has a `domain` foreign-key field which is referenced here. It is irrelevant for the current problem though.
if type is not None:
return RRset.objects.filter(domain__name=name, domain__owner=self.request.user.pk, type=type)
else:
return RRset.objects.filter(domain__name=name, domain__owner=self.request.user.pk)
In urls.py, I have (among others):
url(r'^domains/(?P<name>[a-zA-Z\.\-_0-9]+)/rrsets/$', RRsetsDetail.as_view(), name='rrsets'),
url(r'^domains/(?P<name>[a-zA-Z\.\-_0-9]+)/rrsets/(?P<type>[A-Z]+)/$', RRsetsDetail.as_view(), name='rrsets-type'),
SerializerMethodField is a read-only field so I do not think it will work unless you set a default value... and you are back to the same problem as with CharField.
To simply things you could get rid of serializers.CreateOnlyDefault:
class RRsetSerializer(serializers.ModelSerializer):
type = serializers.CharField(read_only=True, default=TypeDefault())
If you want something more dynamic, I can only think of something like this:
class FromContext(object):
def __init__(self, value_fn):
self.value_fn = value_fn
def set_context(self, serializer_field):
self.value = self.value_fn(serializer_field.context)
def __call__(self):
return self.value
class RRsetSerializer(serializers.ModelSerializer):
type = serializers.CharField(read_only=True,
default=FromContext(lambda context: context.get('view').kwargs['type'].upper()))
FromContext takes a function during instantiation that will be used to retrieve the value you want from context.
All in all, your second approach above is the correct one:
Use serializers.SerializerMethodField and access self.context from the serializer method:
class SomeSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
def get_type(self, obj):
return self.context['view'].kwargs['type'].upper()
The view, request and format keys are automatically added to your serializer context by all of the DRF generic views (http://www.django-rest-framework.org/api-guide/generic-views/#methods at the end of the section). This works just fine.
If you are creating a serializer instance manually, you will have to pass context=contextDict as an argument, where contextDict is whatever you need it to be (http://www.django-rest-framework.org/api-guide/serializers/#including-extra-context).
As #Michael has pointed out in another answer, the SerializerMethodField will be read only. But going by your first example (type = serializers.CharField(read_only=True.....) this seems to be what you want.
I have a filter where I need to access the request.user. However, django-filter does not pass it. Without using the messy inspect.stack() is there a way to get the current user in the method member_filter below?
class ClubFilter(django_filters.FilterSet):
member = django_filters.MethodFilter(action='member_filter')
class Meta:
model = Club
fields = ['member']
def member_filter(self, queryset, value):
# get current user here so I can filter on it.
return queryset.filter(user=???)
For example this works but feels wrong...
def member_filter(self, queryset, value):
import inspect
request_user = None
for frame_record in inspect.stack():
if frame_record[3] == 'get_response':
request_user = frame_record[0].f_locals['request'].user
print(request_user)
is there maybe a way to add this to some middleware that injects user into all methods? Or is there a better way?
Yes, you can do it, and it's very easy.
First, define __init__ method in your ClubFilter class that will take one extra argument:
class ClubFilter(django_filters.FilterSet):
# ...
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(ClubFilter, self).__init__(*args, **kwargs)
With having your user saved into attribute inside ClubFilter, you can use it in your filter. Just remember to pass current user from your view inside FilterSet.
Try self.request.user.
Why it must work.
you can access the request instance in FilterSet.qs property, and then filter the primary queryset there.
class ClubFilter(django_filters.FilterSet):
member = django_filters.MethodFilter(action='member_filter')
class Meta:
model = Club
fields = ['member']
#property
def qs(self):
primary_queryset=super(ClubFilter, self).qs
return primary_queryset.filter(user=request.user)
I'm trying to override the get_form() method of the CreateView. My web page has two identical forms - one for adding a "registered" team and another for adding an "unregistered" team. If an unregistered team is being added, I want to set the team_name field of the form to "Available". As you can see in my code below, I tried accomplishing this by overriding the get_form() method
class TeamCreateView(LeagueContextMixin, CreateView):
model = Team
form_class = TeamForm
template_name = "leagueapp/editleague.html"
registered = False # the correct value of registered is passed in urls.py depending on the url that gets hit
# Overwrite the get_success_url() method
def get_success_url(self):
return '/league/editleague/' + self.kwargs.get('league_id')
def get_form(self, form_class):
form_kwargs = self.get_form_kwargs()
if not self.registered:
form_kwargs['team_name'] = "Available"
return form_class(**form_kwargs)
but this gives me the error __init__() got an unexpected keyword argument 'team_name'. What am I doing wrong and/or is there a better way to go about this?
Edit:
This is my TeamForm
class TeamForm(ModelForm):
class Meta:
model = Team
fields = ['team_name', 'captain', 'registered', 'team_location', 'phone', 'email', 'team_payment']
widgets = {
'team_name':TextInput(attrs={'class':'form-control input-md'}),
'captain':TextInput(attrs={'class':'form-control input-md'}),
'phone':TextInput(attrs={'class':'form-control input-md'}),
'email':TextInput(attrs={'class':'form-control', 'type':'email'}),
'team_location':TextInput(attrs={'class':'form-control input-md'}),
'team_payment':TextInput(attrs={'class':'form-control'}),
'registered':HiddenInput(),
}
You've defined an __init__ method on TeamForm that does not allow for the argument team_name to be present which is why you get the TypeError when you unpack form_kwargs into that __init__(). Either update the __init__ to accept the new kwarg or rewrite the __init__ to
class TeamForm(forms.Form):
def __init__(self, *args, **kwargs):
#code
You don't pass values to form initialization like that. It sounds like what you want to do is to provide custom initial data for the team name, so you should be updating the initial dict:
if not self.registered:
form_kwargs.setdefault('initial', {}).update(name="Available")
Finally figured out that I could change the get_form method like so:
def get_form(self, form_class):
if self.registered:
myform = super().get_form(form_class)
else:
myform = TeamForm({'team_name':'Available', 'team_payment':0.00, 'registered':False})
return myform
I have one model and I've created a form out of the model using ModelForm. Now, I want to spread the form across two pages. For example, the first three fields will appear on the first page then the user clicks next and the last three fields appear on the second page. Then he clicks submit and the user submitted data is added to the database.
I took a look at the docs for the Form Wizard and it seems like it would work for model forms as well? Can someone confirm this?
And if it does, can someone explain the process of creating a WizardView class.
This example is given in the docs and I don't understand what the second two parameters are. Is form_list just a list of form objects that you've instantiated based on your form classes? And what is **kwargs?
class ContactWizard(SessionWizardView):
def done(self, form_list, **kwargs):
do_something_with_the_form_data(form_list)
return HttpResponseRedirect('/page-to-redirect-to-when-done/')
Thanks in advance for your help!
Say your model has two fields
class AModel( Model ):
fieldA = CharField()
fieldB = CharField()
We want to set each field in a separate step using a FormWizard. So we create two ModelForms, each showing one field:
class Form1( ModelForm ):
class Meta:
model = AModel
fields = ( 'fieldA', )
class Form2( ModelForm ):
class Meta:
model = AModel
fields = ( 'fieldB', )
We call our form wizard AWizard; the url.py entry should look something like
url( r'^$', AWizard.as_view( [ Form1, Form2 ] ) ),
In the implementation of AWizard we need to make sure all the forms write their data to a single instance, which we then save to the database:
class AWizard( SessionWizardView ):
instance = None
def get_form_instance( self, step ):
if self.instance is None:
self.instance = AModel()
return self.instance
def done( self, form_list, **kwargs ):
self.instance.save()
Notice that we override the method get_form_instance. This method returns the model instance the forms bind to.
You might think (I did), that this method creates an instance for the first request (the first step of the wizard), and then keeps using that same instance for all steps.
Actually, it's a little more complicated. For each request a new instance of AWizard is created, which in turn creates a new AModel instance. So, the steps don't share a single instance to start with.
The magic happens when the last form is submitted. At this point all forms are revalidated, each form calls get_form_instance and they end up populating a single AModel instance.
That instance is then saved in done.
Form Wizard is being built into Django 1.4 so is a good way to go about this. It should do what you want, but you may need a couple of tweaks.
Don't worry about the kwargs in done() at the moment - you're not going to need them.
form_list is the list of forms that you want to use for your steps - from urls.py
urlpatterns = patterns('',
(r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])),
)
[ContactForm1, ContactForm2] will be passed to done() as form_list.
What you will need to do is break your ModelForm into separate forms. The easiest way to do this (if you want your model on several forms) is to not use ModelForm but just create your own form. It's pretty easy:
from django import forms
class ContactForm1(forms.Form):
subject = forms.CharField(max_length=100)
sender = forms.EmailField()
class ContactForm2(forms.Form):
message = forms.CharField(widget=forms.Textarea)
Once your forms reflect the portions of your model, just create the views and patterns as described in the docs and set do_something_with_the_form_data(form_list) to a function that completes your model from the form data and then does a save.
You could use ModelForm but - only if you can persuade it to produce different forms for Form Wizard to use for each step - that's going to be the tricky part.
The view proposed by #wuerg did not work for me, I had to do this:
class AWizard( SessionWizardView ):
def dispatch(self, request, *args, **kwargs):
self.instance = AModel()
return super(ApplyWizard, self).dispatch(request, *args, **kwargs)
def get_form_instance( self, step ):
return self.instance
def done( self, form_list, **kwargs ):
self.instance.save()
return HttpResponseRedirect(reverse(thanks))
I had to alter the solution of #wuerg and #madmen to work in my usecase (saving the Model after every step). The big advantage of this approach is that it always uses the same instance of the AModel instead of creating a new instance for every step:
class AWizard(SessionWizardView):
instance = AModel()
def dispatch(self, request, *args, **kwargs):
return super(AWizard, self).dispatch(request, *args, **kwargs)
def get_form_instance(self, step):
return self.instance
def done(self, form_list, **kwargs):
self.save_model()
return render_to_response('done.html')
This was fixed in Django 1.9 with form_kwargs.
I have a Django Form that looks like this:
class ServiceForm(forms.Form):
option = forms.ModelChoiceField(queryset=ServiceOption.objects.none())
rate = forms.DecimalField(widget=custom_widgets.SmallField())
units = forms.IntegerField(min_value=1, widget=custom_widgets.SmallField())
def __init__(self, *args, **kwargs):
affiliate = kwargs.pop('affiliate')
super(ServiceForm, self).__init__(*args, **kwargs)
self.fields["option"].queryset = ServiceOption.objects.filter(affiliate=affiliate)
I call this form with something like this:
form = ServiceForm(affiliate=request.affiliate)
Where request.affiliate is the logged in user. This works as intended.
My problem is that I now want to turn this single form into a formset. What I can't figure out is how I can pass the affiliate information to the individual forms when creating the formset. According to the docs to make a formset out of this I need to do something like this:
ServiceFormSet = forms.formsets.formset_factory(ServiceForm, extra=3)
And then I need to create it like this:
formset = ServiceFormSet()
Now how can I pass affiliate=request.affiliate to the individual forms this way?
Official Document Way
Django 2.0:
ArticleFormSet = formset_factory(MyArticleForm)
formset = ArticleFormSet(form_kwargs={'user': request.user})
https://docs.djangoproject.com/en/2.0/topics/forms/formsets/#passing-custom-parameters-to-formset-forms
I would use functools.partial and functools.wraps:
from functools import partial, wraps
from django.forms.formsets import formset_factory
ServiceFormSet = formset_factory(wraps(ServiceForm)(partial(ServiceForm, affiliate=request.affiliate)), extra=3)
I think this is the cleanest approach, and doesn't affect ServiceForm in any way (i.e. by making it difficult to subclass).
I would build the form class dynamically in a function, so that it has access to the affiliate via closure:
def make_service_form(affiliate):
class ServiceForm(forms.Form):
option = forms.ModelChoiceField(
queryset=ServiceOption.objects.filter(affiliate=affiliate))
rate = forms.DecimalField(widget=custom_widgets.SmallField())
units = forms.IntegerField(min_value=1,
widget=custom_widgets.SmallField())
return ServiceForm
As a bonus, you don't have to rewrite the queryset in the option field. The downside is that subclassing is a little funky. (Any subclass has to be made in a similar way.)
edit:
In response to a comment, you can call this function about any place you would use the class name:
def view(request):
affiliate = get_object_or_404(id=request.GET.get('id'))
formset_cls = formset_factory(make_service_form(affiliate))
formset = formset_cls(request.POST)
...
This is what worked for me, Django 1.7:
from django.utils.functional import curry
lols = {'lols':'lols'}
formset = modelformset_factory(MyModel, form=myForm, extra=0)
formset.form = staticmethod(curry(MyForm, lols=lols))
return formset
#form.py
class MyForm(forms.ModelForm):
def __init__(self, lols, *args, **kwargs):
Hope it helps someone, took me long enough to figure it out ;)
I like the closure solution for being "cleaner" and more Pythonic (so +1 to mmarshall answer) but Django forms also have a callback mechanism you can use for filtering querysets in formsets.
It's also not documented, which I think is an indicator the Django devs might not like it as much.
So you basically create your formset the same but add the callback:
ServiceFormSet = forms.formsets.formset_factory(
ServiceForm, extra=3, formfield_callback=Callback('option', affiliate).cb)
This is creating an instance of a class that looks like this:
class Callback(object):
def __init__(self, field_name, aff):
self._field_name = field_name
self._aff = aff
def cb(self, field, **kwargs):
nf = field.formfield(**kwargs)
if field.name == self._field_name: # this is 'options' field
nf.queryset = ServiceOption.objects.filter(affiliate=self._aff)
return nf
This should give you the general idea. It's a little more complex making the callback an object method like this, but gives you a little more flexibility as opposed to doing a simple function callback.
I wanted to place this as a comment to Carl Meyers answer, but since that requires points I just placed it here. This took me 2 hours to figure out so I hope it will help someone.
A note about using the inlineformset_factory.
I used that solution my self and it worked perfect, until I tried it with the inlineformset_factory. I was running Django 1.0.2 and got some strange KeyError exception. I upgraded to latest trunk and it worked direct.
I can now use it similar to this:
BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
As of commit e091c18f50266097f648efc7cac2503968e9d217 on Tue Aug 14 23:44:46 2012 +0200 the accepted solution can't work anymore.
The current version of django.forms.models.modelform_factory() function uses a "type construction technique", calling the type() function on the passed form to get the metaclass type, then using the result to construct a class-object of its type on the fly::
# Instatiate type(form) in order to use the same metaclass as form.
return type(form)(class_name, (form,), form_class_attrs)
This means even a curryed or partial object passed instead of a form "causes the duck to bite you" so to speak: it'll call a function with the construction parameters of a ModelFormClass object, returning the error message::
function() argument 1 must be code, not str
To work around this I wrote a generator function that uses a closure to return a subclass of any class specified as first parameter, that then calls super.__init__ after updateing the kwargs with the ones supplied on the generator function's call::
def class_gen_with_kwarg(cls, **additionalkwargs):
"""class generator for subclasses with additional 'stored' parameters (in a closure)
This is required to use a formset_factory with a form that need additional
initialization parameters (see http://stackoverflow.com/questions/622982/django-passing-custom-form-parameters-to-formset)
"""
class ClassWithKwargs(cls):
def __init__(self, *args, **kwargs):
kwargs.update(additionalkwargs)
super(ClassWithKwargs, self).__init__(*args, **kwargs)
return ClassWithKwargs
Then in your code you'll call the form factory as::
MyFormSet = inlineformset_factory(ParentModel, Model,form = class_gen_with_kwarg(MyForm, user=self.request.user))
caveats:
this received very little testing, at least for now
supplied parameters could clash and overwrite those used by whatever code will use the object returned by the constructor
Carl Meyer's solution looks very elegant. I tried implementing it for modelformsets. I was under the impression that I could not call staticmethods within a class, but the following inexplicably works:
class MyModel(models.Model):
myField = models.CharField(max_length=10)
class MyForm(ModelForm):
_request = None
class Meta:
model = MyModel
def __init__(self,*args,**kwargs):
self._request = kwargs.pop('request', None)
super(MyForm,self).__init__(*args,**kwargs)
class MyFormsetBase(BaseModelFormSet):
_request = None
def __init__(self,*args,**kwargs):
self._request = kwargs.pop('request', None)
subFormClass = self.form
self.form = curry(subFormClass,request=self._request)
super(MyFormsetBase,self).__init__(*args,**kwargs)
MyFormset = modelformset_factory(MyModel,formset=MyFormsetBase,extra=1,max_num=10,can_delete=True)
MyFormset.form = staticmethod(curry(MyForm,request=MyFormsetBase._request))
In my view, if I do something like this:
formset = MyFormset(request.POST,queryset=MyModel.objects.all(),request=request)
Then the "request" keyword gets propagated to all of the member forms of my formset. I'm pleased, but I have no idea why this is working - it seems wrong. Any suggestions?
I spent some time trying to figure out this problem before I saw this posting.
The solution I came up with was the closure solution (and it is a solution I've used before with Django model forms).
I tried the curry() method as described above, but I just couldn't get it to work with Django 1.0 so in the end I reverted to the closure method.
The closure method is very neat and the only slight oddness is that the class definition is nested inside the view or another function. I think the fact that this looks odd to me is a hangup from my previous programming experience and I think someone with a background in more dynamic languages wouldn't bat an eyelid!
I had to do a similar thing. This is similar to the curry solution:
def form_with_my_variable(myvar):
class MyForm(ServiceForm):
def __init__(self, myvar=myvar, *args, **kwargs):
super(SeriveForm, self).__init__(myvar=myvar, *args, **kwargs)
return MyForm
factory = inlineformset_factory(..., form=form_with_my_variable(myvar), ... )
I'm a newbie here so I can't add comment. I hope this code will work too:
ServiceFormSet = formset_factory(ServiceForm, extra=3)
ServiceFormSet.formset = staticmethod(curry(ServiceForm, affiliate=request.affiliate))
as for adding additional parameters to the formset's BaseFormSet instead of form.
based on this answer I found more clear solution:
class ServiceForm(forms.Form):
option = forms.ModelChoiceField(
queryset=ServiceOption.objects.filter(affiliate=self.affiliate))
rate = forms.DecimalField(widget=custom_widgets.SmallField())
units = forms.IntegerField(min_value=1,
widget=custom_widgets.SmallField())
#staticmethod
def make_service_form(affiliate):
self.affiliate = affiliate
return ServiceForm
And run it in view like
formset_factory(form=ServiceForm.make_service_form(affiliate))