Django Class Based View With ModelChoiceField - django

I've been working with Django for about 3 months now and feel I'm getting a bit better, working my way up to class based views. On the surface they seem cleaner and easier to understand and in some cases they are. In others, not so much. I am trying to use a simple drop down view via ModelChoiceField and a form. I can get it to work with a function based view as shown below in my views.py file:
def book_by_name(request):
form = BookByName(request.POST or None)
if request.method == 'POST':
if form.is_valid():
book_byname = form.cleaned_data['dropdown']
return HttpResponseRedirect(book_byname.get_absolute_url1())
return render(request,'library/book_list.html',{'form':form})
Here is my form in forms.py:
class BookByName(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Book.objects.none())
def __init__(self, *args, **kwargs):
super(BookByName, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
self.fields['dropdown'].queryset = Book.objects.order_by('publisher')
This code works. When I have tried to convert to a Class Based View, that's when the trouble begins. I tried to do something like this in views.py:
class BookByNameView(FormView, View):
form_class = BookByName
initial = { 'Book' : Book }
template_name = 'library/book_list.html'
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request, self.template_name, {'form': form})
def get_success_url(self, *args):
return reverse_lazy('library:book_detail', args = (self.object.id,))
When using this with the same form, I receive an attribute error,
'BookByNameView' object has no attribute 'object'.
I've tried ListView as well and received several other errors along the way. The get_success_url also needs to take in a primary key and I can't figure out how to get that passed in as well. Again, I'm a 3 month Django newbie so please be gentle and thanks in advance for your thoughts and suggestions! I feel like I'm in the ballpark...just can't find my seat! I'm very open to doing this differently, if there's a cleaner/better way to do this!
Based on the latest feedback, it would appear the Class Based View should look like:
class BookNameView(FormView):
form_class = BookName
template_name = 'library/book_list.html'
def get_success_url(self, *args):
return reverse_lazy('library:book_detail')
Is this correct? I ran a test version of this and in response to your question as to why I am using self.object.id at all, I am trying to get the pk from the modelchoicefield that I am using to return the view I am trying to get. This may be where I am getting a bit lost. I am trying to get the detail view from the modelchoicefield dropdown, and return the book that is selected. However, I can't seem to pass the pk to this view successfully.
I updated my code to...
class BookByNameView(FormView, ListView):
model = Book
form_class = BookByName
template_name = 'library/book_list.html'
def get_success_url(self, *args):
return reverse_lazy('library:book_detail')
But now it says error...Reverse for 'book_detail' with no arguments not found.

Why are you using self.object there at all? You used form.cleaned_data in the original view, that's what you should use in the class based version too. Note that the form is passed to form_valid.
Note that you've done lots of other weird things too. Your getmethod is pointless, as is your definition of the initial dict; you should delete them both. Also, FormView already inherits from View, there's no need to have View in your declaration explicitly.

You can override the form_valid() function in FormView to achieve what you want. If the form is valid then it is passed to the form_valid() function.
Try this:
class BookByNameView(FormView):
model = Book
form_class = BookByName
template_name = 'library/book_list.html'
def form_valid(self, form):
bookbyname = form.cleaned_data['dropdown']
return HttpResponseRedirect(bookbyname.get_absolute_url())

Related

Django: Display user's previous choices for a ModelForm in the template

I am trying to create a user profile page where users can see and update their preferences for certain things, like whether they are vegetarian, or have a particular allergy, etc. I want the data to be displayed as a form, with their current preferences already populating the form fields.
So I've created the following Model:
class FoodPreferences(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) # One user has one set of food prefs
vegetarian = models.BooleanField()
vegan = models.BooleanField()
...
that's referenced in my forms.py:
class FoodPreferencesForm(forms.ModelForm):
class Meta:
model = FoodPreferences
exclude = ('user', )
I've tried creating a view that inherits FormView and then referencing the form, like this:
class UserProfileView(generic.FormView):
template_name = "registration/profile.html"
form_class = FoodPreferencesForm
success_url = reverse_lazy('user_profile')
This saves the form to a instance of the model correctly, but obviously it just displays the blank form again, after updating, so the user has no idea what their current preferences are.
To implement this I thought I might need to override get() and post() to get the instance of FoodPreferences for the user, and then pass those values into the form like you would a request.POST object. However, firstly, I don't know how to do that, and secondly I'd be taking responsibility for correctly updating the database, which the FormView was already doing.
This is what I've got for that solution:
def get(self, request, *args, **kwargs):
prefs = FoodPreferences.objects.get(user=request.user)
form = self.form_class(prefs)
return render(request, self.template_name, {'form': form, })
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if not form.is_valid():
return render(request, self.template_name, {'form': form, 'error': 'Something went wrong.'})
curr_prefs = FoodPreferences.objects.update_or_create(form.fields)
prefs.save()
return render(request, self.template_name, {'form': form, })
but I get a TypeError: argument of type 'FoodPreferences' is not iterable on the line in get():
form = self.form_class(prefs)
because it's not expecting a model instance.
Am I thinking about this in the right way? This seems like a common enough problem that Django would have something inbuilt to do it, but I can't find anything.
You should only rarely need to define get or post in a class-based view, and you definitely don't here.
To start with, you need to use a more appropriate base class for your view. Here you want to update an existing item, so you should use UpdateView.
Secondly, you need to tell the class how to get the existing object to update, which you can do by definining get_object. So:
class UserProfileView(generic.UpdateView):
template_name = "registration/profile.html"
form_class = FoodPreferencesForm
success_url = reverse_lazy('user_profile')
def get_object(self, queryset=None):
return self.request.user.foodpreferences
# or, if you aren't certain that the object already exists:
obj, _ = FoodPreferences.objects.get_or_create(user=self.request.user)
return obj

Class Based FormView

I have researched this issue for a couple of days and can't seem to find what I'm looking for exactly. I have searched ModelChoiceField as well as ChoiceField on StackOverflow as well as Google and there are many variations of my question but nothing exactly. In a nutshell, I am trying to use a Class Based FormView and then capture the user selection and pass it to a Class Based ListView. Here is my code.
Forms.Py
class BookByStatus(forms.Form):
dropdown = forms.ChoiceField(choices=[],required=False)
def __init__(self, user, *args, **kwargs):
super(BookByStatus, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
self.fields['dropdown'].choices =
Book.objects.values_list("author","author").distinct("Publisher")
The code above works fine, and shows me the output I'm looking for on my view. No issues there....Then I have my FormView...
class BookByStatusView(LoginRequiredMixin,FormView):
model = Book
form_class = BookByStatus
template_name = 'xyz123/publisher.html'
success_url = reverse_lazy('Book:book_by_list',kwargs=
{'dropdown':'dropdown'})
def get_form_kwargs(self):
kwargs = super(BookByStatusView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
self.request.POST['dropdown']
BookByStatusView = form.cleaned_data['dropdown']
return super(BookByStatusView, self).form_valid(form)
The code above works fine, but takes me to the ListView below which I can't seem to pass the dropdown value to....I've tried several different iterations of get_form_kwargs as well as changed my form to ModelChoiceField, but still can't seem to understand how to get a queryset based on the input from the user...
And finally the ListView...
class BookByStatusListView(LoginRequiredMixin,ListView):
model = Book
form_class = BookByStatus
context_object_name = 'book_list'
template_name = 'xyz123/book_by_status_list.html'
paginate_by = 15
def get_queryset(self, *kwargs):
form = self.form_class(self.request.GET)
dropdown = self.kwargs.get('dropdown', None)
if form.is_valid():
return Book.objects.filter(dropdown__icontains=form.
cleaned_data['dropdown'])
return Book.objects.all()
I'm trying to take the dropdown input from the FormView and then pass it to a list view using two separate views. I need to pass the value from the FormView to the ListView. I'm clear on how to get the data in the FormView in the ChoiceField, and how to display a ListView, but I can't seem to figure out how to pass the dropdown data from the FormView to the ListView. I can get the ListView to work, but only with the full queryset, not with a filtered one.
Here's the book model....
class Book(models.Model):
Author CHOICES = (
("New","New"),
("Old","Old"),
)
Author = models.CharField(choices=Author_CHOICES,max_length=10)
Here's the URL...
url(r'^book_by_list/(?
P<dropdown>\w+)/$',views.BookByStatusListView.as_view(),
name='book_by_list'),
Thanks in advance for any suggestions!
Updated Approach...Using request.session. My prior approach would not let me pass the value from the one view to the other, no matter how many get_context_data or get_form_kwargs combinations I tried. Based on the input I received, I began exploring the request.session approach and I've gotten much further. One last piece remains, getting the request.session value in my LISTVIEW so I can filter my querysets accordingly.
class BookByStatusView(LoginRequiredMixin,FormView):
model = Book
form_class = BookByStatus
template_name = 'xyz123/publisher.html'
success_url = reverse_lazy('Book:book_by_list')
def form_valid(self, form):
self.request.session['dropdown'] = form.cleaned_data['dropdown']
return super(BookByStatusView, self).form_valid(form)
def get_form_kwargs(self):
kwargs = super(BookByStatusView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
And in my html template, I leverage the request.session value as...
{{ request.session.dropdown }}
And I reverted the URL back to..
url(r'^book_by_list/$,views.BookByStatusListView.as_view(),
name='book_by_list'),
The last remaining piece is to figure out how to leverage the LISTVIEW with this approach.
My current Listview:
class BookByStatusListView(LoginRequiredMixin,ListView):
model = Book
form_class = BookByStatus
context_object_name = 'book_list'
template_name = 'xyz123/book_by_status_list.html'
paginate_by = 15
def get_queryset(self):
queryset = Book.objects.none()
dropdown = self.request.session.get('dropdown')
if dropdown == 'New':
queryset = Book.objects.all()
elif dropdown == 'Old':
queryset = Book.objects.none()
return queryset
I can't seem to figure out how to pass the dropdown value correctly to the ListView so the queryset is displayed properly. Based on my testing, I don't appear to be capturing dropdown properly in the get_queryset function. Any ideas?
I figured it out. I updated the get_queryset with the proper syntax. Thanks for all of the help to nudge me in the right direction. Last questions..is this the best way to pass a value from one view to another? Is there a better way to do this? Are there any concerns with this approach?
This doesn't work, because redirect creates a new request/response and data from previous are lost. If I understand what you want correctly, one of the options would be to save the drop-down value to session in BookByStatusView and then retrieve it in BookByStatusListView.
You save to session with:
request.session['dropdown_value'] = form.cleaned_data['dropdown']
and retrieve with:
dropdown_value = request.GET.get('dropdown_value')
Here is How to use session part of Django documentation.
EDIT: You can also pass the value as an url parameter like this:
author = 'michael cricthon'
title = 'kongo'
year = [1999, 2000, 2001]
type = ['electronic', 'print', 'hardcover', 'softcover']
params = '?author={}&title={}&&year={}&type={}'.format(
urllib.parse.quote_plus(author),
urllib.parse.quote_plus(title),
','.join(year),
','.join(type))
return HttpResponseRedirect(reverse('search') + params)
The link would look like this:
../search/?author=michael+crichton&title=kongo&year=1999,2000,2001&type=electronic,print,hardcover,softcover
You get parameters with
author = request.GET.get('author')
title = request.GET.get('title')
... etc.

Django form text field prefilling with data from other form

I'm running into a very strange issue where one form is initializing with the data from another form entirely. Here is the first view:
class UpdateProfileView(FormMixin, DetailView):
form_class = UpdateProfileForm
model = Profile
template_name = 'profile/update.html'
def get_context_data(self, **kwargs):
self.object = self.get_object()
context = super(UpdateProfileView, self).get_context_data(**kwargs)
...
self.initial['description'] = profile.about
context['form'] = self.get_form()
return context
...
This is the form that will return the correct data. As soon as it is loaded, however, the following form will return the initialized data from the previous one, even from different sessions, browsers, and locations:
class BountyUpdateForm(forms.ModelForm):
class Meta:
model = Bounty
fields = ("description", "banner")
class UpdateBountyView(UpdateView):
form_class = BountyUpdateForm
model = Bounty
template_name = 'bounty/update.html'
...
def get_context_data(self, **kwargs):
context = super(UpdateBountyView, self).get_context_data(**kwargs)
description = context['form']['description']
value = description.value()
# Value equals what was initialized by the previous form.
I'm really curious why these two forms are interacting in this way. Both form fields are called 'description', but that doesn't explain why the initial data from one would be crossing over to the other. Restarting the server seems to temporarily get the second form to show the correct values, but as soon as the first one is loaded, the second follows suit.
Any help would be greatly appreciated!
After some more searching, I was able to determine that my second view was having self.initial set to the same values as the first form by the time dispatch was being run. I couldn't determine why, but found these related questions:
Same problem, but no accepted answer:
Django(trunk) and class based generic views: one form's initial data appearing in another one's
Different problem, but good answer:
Setting initial formfield value from context data in Django class based view
My workaround was overriding get_initial() on my first form, instead of setting self.initial['description'] directly.
class UpdateProfileView(FormMixin, DetailView):
form_class = UpdateProfileForm
model = Profile
template_name = 'profile/update.html'
def get_initial(self):
return {
'description': self.object.about
}
def get_context_data(self, **kwargs):
...
# Removed the following line #
# self.initial['description'] = profile.about
...
context['form'] = self.get_form()
return context
Hope this helps anyone else who runs into this same problem. I wish I knew more about Django class-based views to be able to understand why this happens to begin with. However, I was unable to determine where self.initial was being set, beyond the empty dict in FormMixin().

Django FormView success_url parameter advice

I am converting an old project with Class Views and I want to know if the pattern I am using is "safe".
In brief I have a class View like this (code simplified ):
class FileAddHashedView(FormView):
"""This view hashes the file contents using md5"""
form_class = FileUploadForm
success_url = reverse_lazy('vault-show')
template_name = "vault/add.html"
filebox_random= 0
def get_success_url(self):
return reverse('vault-show', kwargs={'random': self.filebox_random})
def form_valid(self, form):
instance = form.save(commit=False)
#generate a random number
rng = random.SystemRandom()
#TODO: must catch exception here
instance.random=rng.randint(0, sys.maxint)
instance.save()
#TODO: check what is the proper way to generate the parametric URL
self.filebox_random=instance.random
messages.success(
self.request, 'File hashed and uploaded!', fail_silently=True)
return super(FileAddHashedView, self).form_valid(form)
def form_invalid(self, form, **kwargs):
messages.error(self.request, 'Upload failed ...', fail_silently=True)
return super(FileAddView, self).form_invalid(form)
And the corresponding views that gets activated:
class VaultStatus(TemplateView):
template_name = 'vault/vault.html'
def get(self, request,random):
# retrieve object info
data = {
'filebox': FileBox.objects.get(random=random)
}
return render(request,self.template_name,data)
#return render_to_response('vault/vault.html', {'random': random});
Now my question is: is there a better way in get_success_url to get the self.filebox_random without relying on a class attribute?
Is this thread-safe?
You're not using a class attribute here. Even though you've defined filebox_random at class level, by setting a value to that name within an instance method in Python you're actually creating an instance attribute with the same name, which hides the class-level one. (Although I'm not actually sure why you're defining the class attribute in the first place).
Django does quite a lot of work behind the scenes to ensure that class-based views are thread-safe, so there is no danger in setting instance attributes.

Django Success_url

I built a FormView with two fields in it. I want to use the form to pass queries to a view.
class SectorForm(forms.Form):
date = forms.DateField()
days = forms.IntegerField(max_value=365, min_value=1)
Is there a way I can take my FormView and build the success_url from the valid form fields?
So overriding the get_success_url.
def get_success_url(self):
return reverse_lazy('queryable-view' date days)
I thought I could pull it out from form_valid but never had success.
get_success_url doesn't have access to the Form instance, but form_valid does. Try something like this:
from django.shortcuts import redirect
class YourFormView(FormView):
# ...
def form_valid(self, form):
date, days = form.cleaned_data['date'], form.cleaned_data['days']
return redirect('queryable-view', date, days)
I had similar problem once. When you work with forms, you may not use kwargs and get_success_url doesn't have access to the Form (as told before)
I have get around as:
class YourView(CreateView):
appointment_id =0
def form_valid(self,form):
appointment = form.save()
self.appointment_id = appointment.pk
print self.appointment_id
return super(AppointmentCreate,self).form_valid(form)
def get_success_url(self, **kwargs):
if kwargs != None:
return reverse_lazy('payment', kwargs = {'appid': self.appointment_id})