django class based view multiple form validatiton - python

I'm currently trying to have two forms on a single page. I'm using Class Based Views.
class TaskDetailView(FormMixin, generic.DetailView):
model = Task
template_name="tasks/detail.html"
form_class = NoteForm
form_class2 = DurationForm
def get_context_data(self, **kwargs):
context = super(TaskDetailView, self).get_context_data(**kwargs)
context['note_form'] = self.get_form()
context['notes'] = Note.objects.filter(task__slug=self.kwargs['slug'])
context['duration_form'] = self.form_class2()
context['duration'] = Duration.objects.all()
return context
def get_success_url(self):
return reverse('task_detail', kwargs={'slug': self.kwargs['slug']})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
current_task = get_object_or_404(Task, slug=self.kwargs['slug'])
self.object = form.save(commit=False)
self.object.task = current_task
self.object.save()
return HttpResponse(self.get_success_url())
My current status is that I'm able to display all the forms and save ONLY the Notes form. I'm not able to save the Duration Form despite there's a 200 status POST, the data is not being saved to the database.
I think my mistake is that I'm not validating it but I'm really not sure how to, and there isn't much information on multiple forms on CBVs in Django.
I would really appreciate some guidance and assistance.
Thanks

Your post method doesn't do anything with the second form. You'd need to instantiate it and check its validity as you do with the first one.

A quite simple way of having multiple forms on one page would be to define some hidden parameter to differentiate between your POST actions, like:
<input name="formType" type="hidden" value="note">
In your CBV post method, you could:
form_type = request.POST.get('formType', None)
if form_type == 'note':

Related

I am getting ValueError at /car/offer/4/ Cannot assign "4": "CarRent.car" must be a "Car" instance. when trying to save Car instance to form

I am working on a car rental website for uber drivers in django, from the detailView I need drivers to be able to choose the duration of their rental, and other information will be auto filled to the form from my views.py, i was able to get the driver through request.user, i also need the PK of the car to be rented. searching through here i’ve tried various suggestions by people here, but i keep getting one error after another…
using
self.kwargs['pk'] results in ValueError at /car/offer/4/ Cannot assign "4": "CarRent.car" must be a "Car" instance.
then i tried using
form.car = Car.objects.get(pk= self.kwargs.get('pk')) which results in a AttributeError at /car/offer/4/ 'CarRent' object has no attribute 'is_valid'
can someone please tell me how to get the car instance saved in the CarRent model? any help will be greatly appreciated. Thanks
below is my code (reduced to the relevant bit)
models.py
class Car(models.Model):
car_owner = models.ForeignKey(User, related_name='car_owner', on_delete=models.CASCADE)
class CarRent(models.Model):
car = models.ForeignKey(Car, related_name='rented_car', on_delete=models.CASCADE)
driver = models.ForeignKey(User, related_name='driver_renting', on_delete=models.CASCADE)
rented_weeks = models.BigIntegerField(default=1, choices=WEEK_CHOICES)
forms.py
class RentForm(forms.ModelForm):
class Meta:
model = CarRent
fields = ['rented_weeks']
i’m only displaying the rented weeks as that’s the only information i need from the user.
views.py
class CarView(FormMixin, DetailView):
model = Car
form_class = RentForm
def get_success_url(self):
return reverse('car-details', kwargs={'pk': self.object.pk})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
form = self.get_form()
form = form.save(commit=False)
form.car = self.kwargs['pk']
form.driver = request.user
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
form.save()
return super().form_valid(form)
form.car expects a Car object, not a string with the primary key, but you can simply use:
from django.contrib.auth.mixins import LoginRequiredMixin
class CarView(LoginRequiredMixin, FormMixin, DetailView):
# …
def post(self, request, *args, **kwargs):
form = self.get_form()
self.object = self.get_object()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
form.instance.car_id = self.kwargs['pk']
form.instance.driver = self.request.user
form.save()
return super().form_valid(form)
Note: You can limit views to a class-based view to authenticated users with the
LoginRequiredMixin mixin [Django-doc].
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Django: validation of restricted Foreign keys forms in Mixins Class views

Context: how I handled foreign keys restrictions on GET
I have some trouble validating this form:
class RecordConsultationForm(forms.ModelForm):
class Meta:
model = Consultation
fields = ["fk_type", "fk_client", "duration", "date"]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(RecordConsultationForm, self).__init__(*args, **kwargs)
self.fields['fk_client'].queryset = Client.objects.filter(fk_owner=self.user) # <=====HERE
The queryset restricts the available clients to users. Pretty effective, I just had to add the following to get_context_data():
#method_decorator(login_required, name='dispatch')
class BrowseConsultations(BrowseAndCreate):
template_name = "console/browse_consultations.html"
model = Consultation
form_class = RecordConsultationForm
success_url = 'browse_consultations'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = self.form_class(user = self.request.user) #<=====HERE
return context
def post(self, request):
form = self.form_class(user = self.request.user) #<=====HERE
return super().post(request)
Form validation on BrowseConsultations
Despite what I added in get_context_data() and post(), form data on POST does not seem valid, as if user were None (so it seems):
Invalid fk_client: Select a valid choice. 2 is not one of the available choices.
Maybe get_context_data() was not the proper place to set the user? Is there a way I could tweak the form in post()? Is it the proper way to do it?
Additional details
BrowseAndCreate BrowseConsultations inherits from is a Mixing class I created to handle common ordering tasks and messages. Here is a portion of it:
#method_decorator(login_required, name='dispatch')
class BrowseAndCreate(CreateView, TemplateView):
"""display a bunch of items from a model and the form to create new instances"""
def post(self, request):
super().post(request)
return redirect(self.success_url)
def form_valid(self, form):
super().form_valid(form)
messages.add_message(self.request, messages.SUCCESS,
"Recorded in {}".format(self.object.status))
def form_invalid(self, form):
for e in form.errors.items():
messages.add_message(self.request, messages.WARNING,
"Invalid {}: {}".format(e[0], e[1][0]))
Environment
django 3.0.4
python 3.7
First of all, the CreateView (that your BrowseAndCreate inherits from) handles form validation in the post method, where it calls form_valid on success and form_invalid on failure. Both these methods should return an HTTP response.
Furthermore, the get_context_data from FormMixin that you are overriding already takes care of getting the form data.
If you need the user in your form, you could have this in your form:
class RecordConsultationForm(forms.Form):
def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
And this in your view:
class BrowseConsultations(BrowseAndCreate):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs

Prepopulate ModelForm fields based on referring URL

I have a website which catalogs local hikes. Users can "log" that they have completed these hikes. I have both of these models+forms working as intended. Right now, though, in order to log a hike, you have to select the hike from a long list which contains all the hikes in the database. I'd like to be able to pre-populate that field so that if you are coming from the detail page of the hike in question, then that field is filled in with the hike.
Here's some code:
models.py:
model Hike(models.Model):
name = CharField(max_length=255)
slug = models.SlugField(unique=True)
...other fields...
model UserLog(models.Model):
hike = models.ForeignKey(Hike, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
forms.py:
class LogHike(forms.ModelForm):
class Meta:
model = UserLog
fields = ('hike', 'date', ... other fields)
views.py:
def hike_detail(request, slug):
hike = Hike.objects.get(slug=slug)
log = UserLog.objects.filter(hike__slug=slug)
... more stuff here ...
return render(request, 'hikes/hike_detail.html' {
'hike': hike,
'log': log,
})
def log_hike(request):
if request.method == "POST":
form = LogHike(request.POST)
if form.is_valid():
obj = form.save(commit=False)
userid = request.user
obj.user = request.user
obj.save()
return redirect('user_profile', uid=userid.id)
else:
form = LogHike()
return render(request, 'log_hike.html', {'form': form})
So if a user is viewing the "hike_detail" view, I want to have a link that sends them to the "log_hike" view, but with the "Hike" field pre-populated based on the "Hike" that they came from. I think it might have something to do with the instance function? But I don't have a ton of experience with it. Is there an easy way to pass the data from the referring page in order to pre-populate the form?
You probably want to override your ModelForm __init__ method:
def __init__(self, *args, **kwargs):
super(LogHike, self).__init__(*args, **kwargs)
if 'hike' in kwargs:
self.fields['hike'].value = kwargs['hike']
Now all you need is another view which accepts a parameter passed and you're set. Extend your urls.py for that and then do something like:
def log_hike_with_pre_set_hike(request, *args, **kwargs):
if request.method == 'POST':
# see your code
else:
form = LogHike(hike=kwargs['hike'])
return render(request, 'log_hike.html', {'form': form})
Untested code, you might have to adapt it, I come from class-based views so it might be different for you.
You can pre-populate that form in log_hike when the request.method is get in the same way as when in post.
form = LogHike({'hike':hike_id})
The other thing is form where you'll take the hike_id. But that can come from request.GET for example.

Django Class-based Form is not displayed

New in Django. I'm trying to show form, but have instead it:
messag.views.CommentAdd object at 0x037860D0
forms.py:
from django.http import JsonResponse
class AjaxableResponseMixin(object):
"""
Mixin to add AJAX support to a form.
Must be used with an object-based FormView (e.g. CreateView)
"""
def form_invalid(self, form):
response = super(AjaxableResponseMixin, self).form_invalid(form)
if self.request.is_ajax():
return JsonResponse(form.errors, status=400)
else:
return response
def form_valid(self, form):
# We make sure to call the parent's form_valid() method because
# it might do some processing (in the case of CreateView, it will
# call form.save() for example).
response = super(AjaxableResponseMixin, self).form_valid(form)
if self.request.is_ajax():
data = {
'pk': self.object.pk,
}
return JsonResponse(data)
else:
return response
views.py:
class CommentAdd(AjaxableResponseMixin, CreateView):
model = Comment
fields = ['author_name', 'text', 'root']
class ShowTree(ListView):
model = Comment
template_name = 'comment_tree.html'
def get_context_data(self, **kwargs):
context = super(ShowTree, self).get_context_data(**kwargs)
context['comment_form'] = CommentAdd()
return context
It doesn't work because you need to pass an form instance and you are passing a class based view. CreateView is a class based view, not a ModelForm.
It could be easier to create a CreateView like in the example and get the data to build the list in get_context_data()

Send form data from views to template

Edit:
I want the 'success_url' (ie, result.html) to display the 'data' from 'form.process()'. The following code obviously doesn't work.
Can anyone please tell me what's wrong with it or suggest another way to basically view the context 'data' in a template (either in the form of list or dict), ie a better way to display data to the user after a form has been submitted.
Many thanks in advance.
-- urls.py --
url(r'^$', view='main_view'),
url(r'^result/$', view='result_view'),
-- views.py --
class ResultView(TemplateView):
template_name = "result.html"
class MainView(FormView):
template_name = 'index.html'
form_class = UserInputForm
success_url = 'result/'
def form_valid(self, form):
data = form.process()
return super(MainView, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(MainView, self).get_context_data(**kwargs)
context['data'] = data
return context
main_view = MainView.as_view()
result_view = ResultView.as_view()
As far as I understood your question, you want to show the contents of the user submitted form in the result view. Is that correct?
In this case the method get_context_data won't help you at all, because it will only store data in the current context which is in MainView.
The form_valid method of FormView will make a HttpResponseRedirect to the success_url. So the question now is, how can we give the data to this view.
As explained in Django return redirect() with parameters the easiest way would be to put the data into the session. In the result.html-template you could then access this data as explained in Django: accessing session variables from within a template?
Here is the code:
class ResultView(TemplateView):
template_name = "result.html"
class MainView(FormView):
template_name = 'index.html'
form_class = UserInputForm
success_url = 'result/'
def form_valid(self, form):
self.request.session['temp_data'] = form.cleaned_data
return super(MainView, self).form_valid(form)
in the result.html template you could then access this temp_data so:
{{ request.session.temp_data }}
As suggested above, you can override get_context_data.
For example, you can do something like the below:
def get_context_data(self, **kwargs):
context = super(MainView, self).get_context_data(**kwargs)
#set some more context below.
context['foo'] = bar
...
return context
Look for get_context_data in context the Django class-based view docs. The dict returned by the overridden method will be passed into the templates.
There are a couple of things that could be your problem. First, in form_valid() method, you process the form before you call that class' parent form_valid(). Also, you're no storing the result in a common place for both methods to grab it. Try something like:
def form_valid(self, form):
self.data = form.cleaned_data
return super(MainView, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(MainView, self).get_context_data(**kwargs)
context['data'] = self.data
return context

Categories

Resources