Handling POST request when using ModelChoiceView - django

My simple form looks like this:
class PropertyFilterForm(forms.Form):
property_type = forms.ModelChoiceField(queryset=Property.objects.values_list('type', flat=True).order_by().distinct())
property_type returns a flat list of String values to the drop-down list in my template.
Now, when i choose one of the values and hit "Submit" - i get the following error:
Select a valid choice. That choice is not one of the available
choices.
My view at the moment looks like this:
def index(request):
if request.method == 'POST':
form = PropertyFilterForm(request.POST)
if form.is_valid():
selected_type = form.cleaned_data['property_type']
properties = Property.objects.filter(type=selected_type)
else:
form = PropertyFilterForm()
properties = Property.objects.all()
return render(request, 'index.html', context=locals())
I read and re-read this question many times. It seems to be the same thing, but still I wasn't able to figure out the exact solution.
What I understood so far, is that I need to either explicitly specify a queryset for my form each time I call it in the view, or (better) specify the queryset in the init method of my form.
Could please someone elaborate on whether we need to specify a queryset the way i described above?
If yes, why? Haven't we already specified it in the form definition?
I would be really grateful for any code snippets

You want the user to select a string, not a property instance, so I think it would be a better fit to use a ChoiceField instead of a ModelChoiceField.
class PropertyFilterForm(forms.Form):
property_type = forms.ChoiceField(choices=[])
def __init__(self, *args, **kwargs):
super(PropertyFilterForm, self).__init__(*args, **kwargs)
self.fields['property_type'].choices = Property.objects.values_list('type', 'type').order_by('type').distinct()
The disadvantage of using a ChoiceField is we need to generate the choices in the __init__ method of the form. We have lost the nice functionality of the ModelChoiceField, where the queryset is evaluated every time the form is created.
It's not clear to me why Daniel recommended sticking with ModelChoiceField rather than ChoiceField. If you were to use ModelChoiceField, I think you'd have to subclass it and override label_from_instance. As far as I know, using values() is not going to work.
To specify the initial value, you can either hardcode it in the form definition,
class PropertyFilterForm(forms.Form):
property_type = forms.ChoiceField(choices=[], initial='initial_type')
or set it in the __init__ method,
self.fields['property_type'].initial = 'initial_type'
or provide it when instantiating the form:
form = PropertyFilterForm(request.POST, initial={'property_type': 'initial_type'})

Related

Django: how to re-create my function-based view as a (Generic Editing) Class Based View

I have a function-based view that is currently working successfully. However, I want to learn how to create the equivalent Class Based View version of this function, using the generic UpdateView class -- though I imagine the solution, whatever it is, will be the exact same for CreateView, as well.
I know how to create and use Class Based Views generally, but there is one line of my function-based view that I have not been able to work into the corresponding UpdateView -- as usual with the Generic Editing Class Based Views, it's not immediately clear which method I need to override to insert the desired functionality.
The specific task that I can't port-over to the CBV, so to speak, is a line that overrides the queryset that will be used for the display of a specific field, one that is defined as ForeignKey to another model in my database.
First, the working function-based view, with highlight at the specific bit of code I can't get working in the CVB version:
#login_required
def update_details(request, pk):
"""update details of an existing record"""
umd_object = UserMovieDetail.objects.select_related('movie').get(pk=pk)
movie = umd_object.movie
if umd_object.user != request.user:
raise Http404
if request.method != 'POST':
form = UserMovieDetailForm(instance=umd_object)
# this is the single line of code I can't get working in UpdateView version:
form.fields['user_guess'].queryset = User.objects.filter(related_game_rounds=movie.game_round)
else:
form = UserMovieDetailForm(instance=umd_object, data=request.POST)
if form.is_valid():
form.save()
return redirect(movie)
context = {'form': form, 'object': umd_object }
return render(request, 'movies/update_details.html', context)
I can recreate every part of this function-based view in UpdateView successfully except for this line (copied from above for clarity):
form.fields['user_guess'].queryset = User.objects.filter(related_game_rounds=movie.game_round)
What this line does: the default Form-field for a ForeignKey is ModelChoiceField, and it by default displays all objects of the related Model. My code above overrides that behavior, and says: I only want the form to display this filtered set of objects. It works fine, as is, so long as I'm using this function-based view.
Side-Note: I am aware that this result can be achieved by modifying the ModelForm itself in my forms.py file. The purpose of this question is to better understand how to work with the built-in Generic Class Based Views, enabling them to recreate the functionality I can already achieve with function-based views. So please, refrain from answering my question with "why don't you just do this in the form itself instead" -- I am already aware of this option, and it's not what I'm attempting to solve, specifically.
Now for the UpdateView (and again, I think it would be the same for CreateView). To start off, it would look essentially like this:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
model = UserMovieDetail
template_name = 'movies/update_details.html'
form_class = UserMovieDetailForm
login_url = 'login' # used by LoginRequiredMixin
# what method do I override here, to include that specific line of code, which needs
# to occur in the GET portion of the view?
def get_success_url(self):
return reverse('movies:movie', kwargs={'pk': self.object.movie.pk, 'slug': self.object.movie.slug })
The above is a working re-creation of my function-based view, replicating all the behavior except that one important line that filters the results of a specific field's ModelChoiceField display in the Form.
How do I get that line of code to function inside this UpdateView? I've reviewed the methods built-in to UpdateView on the classy class-based views website, and then attempted (by pure guess-work) to over-ride the get_form_class method, but I it didn't work, and I was basically shooting in the dark to begin with.
Note that since the functionality I want to re-create is about the display of items in ModelChoiceField of the form, the desired behavior applies to the GET portion of the view, rather than the POST. So I need to be able to override the form fields before the form is rendered for the first time, just like I did in my function based view. Where and how can I do this in UpdateView?
First, a note not related to form - from raise Http404 in functional view I understand that you want to allow user to access only his own movies. For that in class based view you can override get_queryset method:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
def get_queryset(self):
return UserMovieDetail.objects \
.filter(user=request.user) \
.select_related('movie')
Now let's move to customizing form.
Option 1 - .get_form()
You can override get_form method of the UpdateView:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
form_class = UserMovieDetailForm
def get_form(self, form_class=None):
form = super().get_form(form_class)
# add your customizations here
round = self.object.movie.game_round
form.fields['user_guess'].queryset = \
User.objects.filter(related_game_rounds=round)
return form
Option 2 - moving customizations to form class and .get_form_kwargs()
You might prefer to move customization logic from view to form. For that you can override form's __init__ method. If customization logic requires extra information (for example, queryset depends on current user), then you can also override get_form_kwargs method to pass extra parameters to the form:
# views.py
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
form_class = UserMovieDetailForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({'current_user': self.request.user})
return kwargs
# forms.py
class UserMovieDetailForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.current_user = kwargs.pop('current_user')
super().__init__(*args, **kwargs)
# add your customizations here
self.fields['user_guess'].queryset = ...
P.S. In general, great resource for understanding django class based views is https://ccbv.co.uk/

ModelChoiceField gives "Select a valid choice" error on submission

Here is my simple form with one ModelChoiceField:
class PropertyFilter(forms.Form):
type = forms.ModelChoiceField(queryset=Property.objects.order_by().values_list('type', flat=True).distinct(),
empty_label=None)
It allows user to choose from one of the options (each represented as a string). When i choose an option and hit "Submit" - it returns:
Select a valid choice. That choice is not one of the available
choices.
My views.py looks like this:
from models import Property
from .forms import PropertyFilter
def index(request):
if request.method == 'POST':
form = PropertyFilter(request.POST)
if form.is_valid():
return HttpResponseRedirect('/')
else:
form = PropertyFilter()
properties = Property.objects.all()
return render(request, 'index.html', context=locals())
What am i doing wrong?
The queryset parameter for ModelChoiceField cannot be values_list, because it's going to save the relationships, so django have to use complete model objects, not certain values of model objects.
If you want to show custom choice text, you should define a simple choice field yourself, in django way. You can subclass the django form ModelChoiceField and override label_from_instance method to return what text you want to show:
class PropertyModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.type
class PropertyFilter(forms.Form):
type = PropertyModelChoiceField(queryset=Property.objects.all())
Something not related, but it's better to use PropertyFilterForm as form name, it will make your code clearer to read. Also type is a reserved word in python, so try to use something else for your field name like property_type would be better.
Edit:
I think you(and I as well) are confused of what's your original intention. You need to choose types of Property from selection, not Property objects, so you need to use ChoiceField instead:
class PropertyFilter(forms.Form):
type_choices = [(i['type'], i['type']) for i in Property.objects.values('type').distinct()]
type = forms.ChoiceField(choices=type_choices)

dynamic manipulation of django admin form

I have a modified admin form, where I added a field that shall modify the values of the current model's parent object. Now, depending on the user, I need to
alter the queryset of that extra field
set another field as readonly (or better, even hide it completely)
Basically my code below works as I'd expect it. A superuser gets the whole queryset and the other field is not readonly. All other users get a limited queryset and the other field is readonly. However, once I open that site in a different browser and as a non-superuser, even the superuser get the same result as the non-superusers. Seems like django somehow caches the result? If I put some print statements inside the conditional branches though, they get printed correctly. So the method still gets called each time and seems to still perform these steps. Only with a wrong outcome.
Is that a caching problem? Am I doing something entirely wrong? Can it be a bug in the django test server?
def get_form(self, request, obj=None, **kwargs):
form = super(MultishopProductAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.declared_fields['categories'].initial = obj.product.category.all()
if not request.user.is_superuser:
user_site = request.user.get_profile().site
form.declared_fields['categories'].queryset = Category.objects.filter(site__id=user_site.id)
self.readonly_fields = ('virtual_sites', )
if obj is not None:
form.declared_fields['categories'].initial = obj.product.category.filter(site__id=user_site.id)
return form
Yes you are doing it wrong. In Django 1.2+ you can use get_readonly_fields.
From this answer:
The ModelAdmin is only instantiated once for all requests that it receives. So when you define the readonly fields like that, you're setting it across the board permanently.
Regarding altering the queryset. From the documentation:
class MyModelAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
To expand on dan-klasson's anwer: Do not ever set instance attributes in any ModelAdmin method (like self.readonly_fields) to prevent issues like the one you described. Only use Django ModelAdmin's options (which are class-level) or methods to manipulate any behavior. Regarding readonly fields, you can use the ModelAdmin.get_readonly_fields method which has this signature: get_readonly_fields(self, request, obj=None).
So I couldn't find a really clever way to do what I wanted with django admin's custom methods. What I ended up doing now is implementing the admin's change_view, setting up my own form manually and performing all my custom initializations from there.
I then provided a custom template by setting change_form_template, which is simply extending admin/change_form.html but rendering my own form instead of the default one. I also set extra_context['adminform'] = None so the default admin form gets removed.
That way I can now customize my form the way I need it to be but still use all the other admin conveniences. So far it seems to work very nicely. Not the very most elegant solution either I think, but the best I could think of.

Any danger in manually setting cleaned_data in Django?

This may be a dumb question, but I'm a bit unsure if it's safe to manually set the cleaned_data. The docs says:
Once is_valid() returns True, you can process the form submission
safe in the knowledge that it conforms to the validation rules defined
by your form. While you could access request.POST directly at this
point, it is better to access form.cleaned_data. This data has not
only been validated but will also be converted in to the relevant
Python types for you.
For more context, say we have a modelform which has several fields such as a book's title, book's author, and a field which asks for a url.
The form conditions are: if the url field is empty, the user must provide the title and author. If the url field is given and nothing else, I would parse the html from the given url and extract the title and author automatically for the user.
In the case where I automatically grab the title and author from the url, what would be the best way to handle saving this data to the model, since the form would return an empty cleaned_data for author and title? I made sure the data parsed will conform to the validate rules I have in the model, but setting cleaned_data like this seems suspicious.
In modelform class:
def save(self, commit = True, *args, **kwargs):
parsed_title = ... # String returned by my html parsing function
parsed_author = ... # String returned by my html parsing function
self.cleaned_data['title'] = parsed_title
self.cleaned_data['author'] = parsed_author
EDIT:
Thanks, I made it like so:
def save(self, commit=True, *args, **kwargs):
instance = super(BookInfoForm, self).save(commit=commit, *args, **kwargs)
....
instance.title = parsed_title
instance.author = parsed_author
return instance
This is a bit off topic since you've already answered the original question, but the above code breaks some other part. Instead of saving the compiled info to http://..../media/books/<id> where <id> is the book id, it saves it to http://..../media/books/None.
I have a add/edit function in my views.py that handles adding and editing:
def insert_or_modify(request, id=None):
if id is not None:
book = BookModel.objects.get(pk=id)
else:
book = BookModel()
if request.method == 'POST':
form = BookInfoForm(request.POST, instance=book)
if form.is_valid():
form.save()
....
return render_to_response(...)
Is there a way to make sure the id is present so that I won't get id=None? I guess more specifically, in the save() in the modelform, is there a way to create a new instance with an id if instance.id = None? Although I thought calling super(ModelForm, self).save(...) would do that for me?
Thanks again!
In the case you present, your intention isn't setting the cleaned_data, but the model data. Therefore, instead of setting cleaned_data in the save method, just set the attributes of self.instance and then save it.
About setting cleaned_data manually, I don't think it's necessarily wrong, it may make sense to do it in the form's clean method for some cross-field validation, although it's not a common case.

Different initial data for each form in a Django formset

Is it possible to prepopulate a formset with different data for each row? I'd like to put some information in hidden fields from a previous view.
According to the docs you can only set initial across the board.
If you made the same mistake as me, you've slightly mistaken the documentation.
When I first saw this example...
formset = ArticleFormSet(initial=[
{'title': 'Django is now open source',
'pub_date': datetime.date.today(),}
])
I assumed that each form is given the same set of initial data based on a dictionary.
However, if you look carefully you'll see that the formset is actually being passed a list of dictionaries.
In order to set different initial values for each form in a formset then, you just need to pass a list of dictionaries containing the different data.
Formset = formset_factory(SomeForm, extra=len(some_objects)
some_formset = FormSet(initial=[{'id': x.id} for x in some_objects])
You need to use the technique described in this post in order to be able to pass parameters in. Credit to that author for an excellent post. You achieve this in several parts:
A form aware it is going to pick up additional parameters
Example from the linked question:
def __init__(self, *args, **kwargs):
someeobject = kwargs.pop('someobject')
super(ServiceForm, self).__init__(*args, **kwargs)
self.fields["somefield"].queryset = ServiceOption.objects.filter(
somem2mrel=someobject)
Or you can replace the latter code with
self.fields["somefield"].initial = someobject
Directly, and it works.
A curried form initialisation setup:
formset = formset_factory(Someform, extra=3)
formset.form = staticmethod(curry(someform, somem2mrel=someobject))
That gets you to passing custom form parameters. Now what you need is:
A generator to acquire your different initial parameters
I'm using this:
def ItemGenerator(Item):
i = 0
while i < len(Item):
yield Item[i]
i += 1
Now, I can do this:
iterdefs = ItemGenerator(ListofItems) # pass the different parameters
# as an object here
formset.form = staticmethod(curry(someform, somem2mrel=iterdefs.next()))
Hey presto. Each evaluation of the form method is being evaluated in parts passing in an iterated parameter. We can iterate over what we like, so I'm using that fact to iterate over a set of objects and pass the value of each one in as a different initial parameter.
Building on Antony Vennard's answer, I am not sure what version of python/django he is using but I could not get the generator to work in the curry method either. I am currently on python2.7.3 and django1.5.1. Instead of using a custom Generator, I ended up using the built-in iter() on a list of things to create an iterator and passing the iterator itself in the curry method and calling next() on it in the Form __init__(). Here is my solution:
# Build the Formset:
my_iterator = iter(my_list_of_things) # Each list item will correspond to a form.
Formset = formset_factory(MyForm, extra=len(my_list_of_things))
Formset.form = staticmethod(curry(MyForm, item_iterator=my_iterator))
And in the form:
# forms.py
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
# Calling next() on the iterator/generator here:
list_item = kwargs.pop('item_iterator').next()
# Now you can assign whatever you passed in to an attribute
# on one of the form elements.
self.fields['my_field'].initial = list_item
Some Key things I found were that you need to either specify an 'extra' value in the formset_factory or use the initial kwarg on the formset to specify a list that corresponds to the list you pass to the iterator (In above example I pass the len() of the my_list_of_things list to 'extra' kwarg to formset_factory). This is necessary to actually create a number of forms in the formset.
I had this problem and I made a new widget:
from django.forms.widgets import Select
from django.utils.safestring import mark_safe
class PrepolutatedSelect(Select):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = ''
if value == '':
value = int(name.split('-')[1])+1
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value])
if options:
output.append(options)
output.append('</select>')
return mark_safe(u'\n'.join(output))
Maybe this will work for you too.
formset = BookFormset(request.GET or None,initial=[{'formfield1': x.modelfield_name1,'formfield2':x.modelfield_name2} for x in model])
formfield1,formfield2 are the names of the formfields.
modelfield_name1,modelfield_name2 are the modal field names.
model is name of your modal class in models.py file.
BookFormset is the form or formset name which is defined in your forms.py file