ModelChoiceField gives "Select a valid choice" error on submission - django

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)

Related

Can I use standard generic CreateView for model formset?

I am again stuck with model formset. I have following form:
ArticleFormSet = modelformset_factory(Article, fields=('title', 'pub_date'), extra=3)
and View for this model formset:
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleFormSet
template_name = 'article_form_view.html'
success_url = "/"
Reading documentation here: https://docs.djangoproject.com/en/4.0/topics/forms/modelforms/#model-formsets
I understand that, I can use ArticleFormSet instead of standard form for form_class attribute.
So basically, what I am expecting is, this should display 3 forms for the same Article model and I should be able to create three articles at the same time.
But I am getting "TypeError: init() got an unexpected keyword argument 'instance'
".
So the question is, is this how it supposed to work? Or what would be the correct way of creating few articles at the same time?
I do not think that the generic CreateView is by default capable of handling formsets; however, it should be easier to customize the view so it works as you intend.
From what I see the exception is raised when the view tries to create the form to render. So overriding the get_form method is required:
class ArticleCreateView(CreateView):
...
def get_form(self):
if self.request.method == 'POST':
return self.form_class(self.request.POST)
return self.form_class()
The method either returns the modelformset as created by the factory (in case of calling the formset) or fills it with your posted data.
One more issue seems to be the success_url. You will get an expception when you use a formset. So you also need to override that method, e.g.
...
def get_success_url(self):
return self.success_url
Now it should work.
However: If I understand you correctly, you want the view to be used for creating new data. The `` modelformset_factory``` fetches all your model instance and appends 3 empty forms. This might not be what you want. In order to get an empty formset look at this SO post:
Empty ModelFormset in Django's FormWizard

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/

Handling POST request when using ModelChoiceView

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'})

Dynamic choices for Django SelectMultiple Widget

I'm building a form (not modelForm) where i'd like to use the SelectMultiple Widget to display choices based on a query done during the init of the form.
I can think of a few way to do this but I am not exactly clear on the right way to do it. I see different options.
I get the "choices" I should pass to the widget in the form init but I am not sure how I should pass them.
class NavigatorExportForm(forms.Form):
def __init__(self,user, app_id, *args,**kwargs):
super (NavigatorExportForm,self ).__init__(*args,**kwargs) # populates the form
language_choices = Navigator.admin_objects.get(id=app_id).languages.all().values_list('language', flat=True)
languages = forms.CharField(max_length=2, widget=forms.SelectMultiple(choices=???language_choices))
Why not use a ModelMultipleChoiceField instead?
You could do simply this :
class NavigatorExportForm(forms.Form):
languages = forms.ModelMultipleChoiceField(queryset=Language.objects.all())
def __init__(self, app_id, *args, **kwargs):
super(NavigatorExportForm, self).__init__(*args, **kwargs)
# Dynamically refine the queryset for the field
self.fields['languages'].queryset = Navigator.admin_objects.get(id=app_id).languages.all()
This way you don't only restrict the choices available on the widget, but also on the field (that gives you data validation).
With this method, the displayed string in the widget would be the result of the __unicode__ method on a Language object. If it's not what you want, you could write the following custom field, as documented in ModelChoiceField reference :
class LanguageMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.language_code # for example, depending on your model
and use this class instead of ModelMultipleChoiceField in your form.
def __init__(self,user, app_id, *args,**kwargs):
super (NavigatorExportForm,self ).__init__(*args,**kwargs)
self.fields['languages'].widget.choices = Navigator.admin_objects.get(id=app_id).languages.all().values_list('language', flat=True)
that seems to do the trick, but even by not specifying a max_length, the widget only display the first letter of the choices...

Newbie Django Question : Can't find data I thought I preset in a Form

I'm still getting to grips with Django and, in particular, Forms.
I created MyForm which subclasses forms.Form in which I define a field like this :
owner = forms.CharField(widget=forms.HiddenInput)
When I create a new, blank instance of the Form I want to prefill this with the creator's profile, which I'm doing like this :
form = MyForm( {'owner' : request.user.get_profile()} )
Which I imagine sets the owner field of the form to the request.user's id. (The type of the corresponding "owner" field in the models.Model class is ForeignKey of Profile.)
Before rendering the form, I need to check one piece of information about the owner. So I try to access form.owner, but there seems to be no "owner" attribute of the form object. I also tried form.instance.owner, but similarly, no luck.
What am I doing wrong? What have I misunderstood?
You can access this value via the form's data dictionary:
form.data.get('owner')
Initial data in a form should be passed in with the initial kwarg.
Once you've turned the form into a bound form (usually by passing request.POST in as the first argument to instantiate the form, the place you are currently incorrectly providing the initial dictionary), and performed validation with form.is_valid(), the data the user submitted will be in form.cleaned_data, a dictionary. If they changed the initial value, their changed value will be in that dictionary. If not, your initial value will be.
If you don't want to let the user modify the value, then don't have it be a field, instead pass it in as a kwarg, and store it as an instance attribute in form.__init__():
class MyForm(Form):
def __init__(self, *args, profile, **kwargs):
super().__init__(*args, **kwargs)
self.profile = profile
...
form = MyForm(
request.POST if request.POST else None,
profile=request.user.get_profile(),
)
if request.method == "POST" and form.is_valid():
do_stuff_with(form.profile)
Also as with most things, this all gets easier if you drink the Django kool-aid and use generic views.
class MyView(FormView):
form_class = MyForm
...
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"profile": self.request.user.get_profile()
}
def form_valid(self, form):
do_stuff_with(form.profile)
return super().form_valid(form)
Or for the initial case whereby you want it to be editable:
class MyView(FormView):
form_class = MyForm
...
def get_initial(self):
return {
**super().get_initial(),
"profile": self.request.user.get_profile()
}
def form_valid(self, form):
do_stuff_with(form.cleaned_data.get("profile"))
return super().form_valid(form)
If MyForm happens to be a form about one single instance of a specific model then this gets even easier with UpdateView instead of FormView. The more you buy into the way Django wants to do things, the less you have to work.