Validate form and formsets with django - python

I'm trying to set my form validation in my view and I would like to get you help because I have a little issue.
This is my formset :
DocumentFormSet = inlineformset_factory(Publication, Document, form=DocumentForm, extra=1, max_num=4, can_delete=True)
This is my code :
def get_context_data(self, **kwargs):
context = super(PublicationCreateView, self).get_context_data(**kwargs)
document_queryset = Document.objects.all()
context['FormSets'] = DocumentFormSet(self.request.POST or None, self.request.FILES or None,
prefix='doc', queryset=document_queryset)
return context
def form_valid(self, form):
try:
context = self.get_context_data()
formsets = context['FormSets']
if form.is_valid():
self.object = form.save(commit=False)
if formsets.is_valid():
formsets.instance = self.object
formsets.save(commit=False)
for element in formsets:
element.save(commit=False)
self.object = form.save()
formsets.save()
return super(PublicationCreateView, self).form_valid(form)
except IntegrityError:
...
I read some things, especially form has to be save before forms in formset if there is ForeignKey between form and forms in my formset.
The issue comes if I have wrong field in my formset. As the first form is saved, it creates an object with only the first form and not with the associated form in my formset.
This is an example :
As you can see, it saves my first form, but I don't want until all the formset is valid.
I have to switch between .save(commit=False) and .save() methods.
Thank you

So, don't save the main form first.
if form.is_valid() and formsets.is_valid():
self.object = form.save()
formsets.instance = self.object
formsets.save()
(Note, you don't need to save the formset forms individually; just remove the commit=False both times.)

Related

What causes "no attribute 'object_list' when I purposely csuse HTML error

I am writing a django ListView with FormMixin, but it can't handle form errors. The model limits input to 140 characters, when I inspect and change limit to 200 and submit I get
'PostListView' object has no attribute 'object_list'"
Here's the code
class PostListView(FormMixin, generic.ListView):
model = Post
form_class = PostForm
paginate_by = 10
template_name = 'index.html'
def get_success_url(self):
return reverse('index')
def post(self, request, *args, **kwargs):
form = PostForm(request.POST)
if form.is_valid():
form.save()
return super().form_valid(form)
else:
return self.form_invalid(form)
With everything working normally, it saves the data and displays the list. On error, no error, it fails.
EDIT
As #crimsonpython24 has said, ListView is for displaying data. I opted to use a basic view
def index(request):
'''deal with post method first'''
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
form.save()
return redirect(reverse('index'))
else:
form = PostForm
posts = Post.objects.all()
return render(request, 'index.html', {'form':form, 'posts':posts})
This allows for invalid form data to be returned for correction, and also allows viewing of posts
The point is that ListView is only supposed to view objects. If you have a form in your view, try to go for one of the edit views, which lets you create, update, and delete (now I'm assuming that you also handle a form in this view).
I can't exactly describe how ListView causes your problem other than it does not fit your purpose and there are better alternatives.
EDIT
Now you're concatenating a FormView and ListView. However, I will still recommend going for a FormView as a ListView doesn't have a form_class attribute. It's easy, though. Let's say you have this FormView class:
class ContactView(FormView):
template_name = 'contact.html'
form_class = ContactForm # Now you can simply do this to your form
success_url = '/thanks/'
def form_valid(self, form):
form.send_email()
return super().form_valid(form)
and then simply pass in context data to make it behave partially like a ListView:
class ContactView(FormView):
# whatever you have here already
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['now'] = timezone.now()
return context
So now your new view will render both your form and context data. Seeing as both these views don't require a primary key to operate, I don't see any problem implmenting this.

How to open detail view in editable format as shown in admin page?

I'm trying to show the user the details they entered in an editable format as we can see in
Edit:
Changed the code and tried to explain the question in a better way
view.py
def change_contact(request, contact_id):
try:
form = AddToPhoneBookForm(instance=Contact.objects.get(pk=contact_id))
form.instance.user = request.user
if form.is_valid():
form.save()
form = AddToPhoneBookForm()
context = {
'form': form
}
return render(request, "CallCenter/add_to_phone_book.html", context)
forms.py
class AddToPhoneBookForm(forms.ModelForm):
class Meta:
model = Contact
fields = ['first_name', 'last_name', 'phone_number', 'phone_book']
This view loads the forms as I want it to but the changes made here is not reflected in the database. Where am I going wrong?
For this you need to use get() instead of filter() .Get returns the single object whereas filter will returns the queryset
contact = Contact.objects.get(phone_book__id=phone_book_id)
And in template you don't need to use forloop {{contact.first_name}} will give the result for you
EDIT: you will save the data with POST request so you need to handle for POST request also and there are a lots of things you need to know please read the docs
And change your view like this
def change_contact(request, contact_id):
contact = Contact.objects.get(pk=contact_id)
form = AddToPhoneBookForm(instance=contact)
if request.method == 'POST':
form = AddToPhoneBookForm(request.POST,instance=contact)
if form.is_valid():
obj=form.save(commit=False)
obj.user = request.user
obj.save()
return redirect('some-path')
context = {
'form': form,'contact':contact
}
return render(request, "CallCenter/add_to_phone_book.html", context)

In django, how can I ensure that a specific user is accessing a view?

I would like to be able to check if the user accessing a page is a specific user.
For instance, when a user accesses the "Edit Post" page on my blog app, I want to ensure that the user is the author of the post.
Currently, I check that the user accessing '/Blog/Edit//' has the blog.change_post permission.
However, if a second user (who also has that permission) were to enter the URL to change someone else's post, they would pass that permission check and be able to edit someone else's post.
What I want is a #user_passes_test function that check's the user object accessing the view against the author attribute of the post.
#/Blog/urls.py
urlpatterns = [
...
path('Edit/<int:pk>', views.BlogEdit, name='BlogEdit'),
...
]
#/Blog/views.py
#permission_required('blog.change_post', login_url='/Portal/login')
def BlogEdit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
post = form.save(commit=False)
post.save()
return redirect('/Blog', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'Blog/Edit.html', {'form': form})
You can add an extra filter to your get_object_or_404:
#permission_required('blog.change_post', login_url='/Portal/login')
def BlogEdit(request, pk):
post = get_object_or_404(Post, pk=pk, author=request.user)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
return redirect('/Blog', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'Blog/Edit.html', {'form': form})
Here author is the hypothetical ForeignKey from Post to the user model. It is possible that the name is different, but the idea is still the same.
This thus means that in case the pk is the pk of a Blog for which request.user is not the author, then we will have a 404 response.
The advantage of filtering here, is that we use a single query for the filtering. We will not (lazily) load the author to check if it is the same as the logged in user.
Note: post = form.save(commit=False) and post.save() are equivalent to post = form.save() (so with commit=True).
In a class based view change the get_queryset method to filter by current user.
class MyView(LoginRequiredMixin, UpdateView):
...
def get_queryset(self):
"""
Only the current logged in user can edit ...
"""
return super().get_queryset().filter(created_by=self.request.user)
I figured it out from This Post in the end. What urbanspaceman suggests works for class based views, but for function based views you can do:
#permission_required('blog.change_post', login_url='/Portal/login')
def BlogEdit(request, pk):
post = get_object_or_404(Post, pk=pk)
if post.author != request.user:
raise Http404("You are not allowed to edit this Post")
# ... Rest of view here ... #

Django Many To Many Management with inline formset

I can't seem to find a good way to solve this situation.
I have two models Office and People with a many to many relationship through a Contact model with additional fields.
Now in my views (CreateView and UpdateView), I am using inline formset to manage the relationship.
My problem here is with UpdateView, how do I update the many to many relationship? I can add new items. But how to delete existing ones? The formset generates a checkbox DELETE but I get lost in the code. How to use it?
One way could be to delete all the corresponding rows in the through model and recreates new ones with data submitted from the form but I believe there should be a more efficient way to do it.
Can someone help?
Here's my current code:
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = OfficeContactInline(request.POST, instance=self.object)
if form.is_valid() and formset.is_valid():
self.object = form.save()
contacts = formset.save(commit=False)
for contact in contacts:
contact.office = self.object
contact.save()
formset.save_m2m()
return HttpResponseRedirect(self.get_success_url())
else:
return render(request, self.template_name, self.get_context_data(form=form, formset=formset))
I finally found a solution to my problem. There actually is a change in behavior with Django 1.7: formset.save(commit=False) no longer deletes deleted items (checkbox checked).
You therefore have to use formset.deleted_objects to do it: working code below
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = OfficeContactInline(request.POST, instance=self.object)
if form.is_valid() and formset.is_valid():
self.object = form.save()
contacts = formset.save(commit=False)
# New with Django 1.7
for del_contact in formset.deleted_objects:
del_contact.delete()
for contact in contacts:
contact.office = self.object
contact.save()
formset.save_m2m()
return HttpResponseRedirect(self.get_success_url())
else:
return render(request, self.template_name, self.get_context_data(form=form, formset=formset))
This is documented here: https://docs.djangoproject.com/en/1.7/topics/forms/formsets/#django.forms.formsets.BaseFormSet.can_delete

Django - make a form remember its checkbox values

I'll address this question using a base setup:
# models.py
class MyModel(models.Model):
required_field = models.CharField("some label", max_length=10)
another_required_field = models.CharField("some label", max_length=10)
checkbox = models.BooleanField("some label")
# forms.py
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
# views.py
class MyView(FormView):
form_class = MyForm
template_name = 'some-template.html'
Now suppose I check the checkbox and fill just one of the required fields. The form obviously doesn't pass validation and gets back with errors and all. Problem is, the value of the checkbox comes back unchecked. This is not a big deal with just one BooleanField, but I'm working on a project where I have tons of checkboxes. Check them all from scratch is rather frustrating.
So I had a check on django's documentation and stumbled upon this paragraph regarding BooleanFields:
Since all Field subclasses have required=True by default, the validation condition here
is important. If you want to include a boolean in your form that can be either True or
False (e.g. a checked or unchecked checkbox), you must remember to pass in
required=False when creating the BooleanField.
And I did this:
# forms.py
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
for field in self.fields:
if isinstance(field, forms.CheckboxInput):
self.fields[field].required = False
class Meta:
model = MyModel
but it didn't work. Again, checkboxes lose their state after the form didn't pass validation, so I guess that was not what I was looking for.
So my question is, is there a way to achieve that? I'm pretty sure there should be one, it would be great if some of you could at least drive me in the right direction. Thanks :-)
EDIT
After a bit of debugging, I solved the issue. Turns out I was using a custom template for crispy forms checkboxes, and I found a little bug in there.
Your view would need to populate the form from the request.POST dictionary as such:
def your_view(request):
form = MyForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
form.save()
# do whatever esle
return render(request, 'your-template.html', {'form': form})
Unless you pass the request.POST data, and/or an instance of the model you're editing, your form will be un-bound, and therefore not show any values that exist either in the POST data or from your model. If you're editing an instance, it would look like:
def your_view(request, id):
my_model_instance = MyModel.objects.get(pk=id)
form = MyForm(request.POST or None, instance=my_model_instance)
if request.method == 'POST' and form.is_valid():
form.save()
# do whatever esle
return render(request, 'your-template.html', {'form': form})
maybe the problem is on your view:
view exemple:
def view(request):
if request.method == 'POST':#bound the form wit data from request.Post
form = MyForm(request.POST)
if form.is_valid():
#do somthing
form.save()
#if form not valid render the page.html with form that has request.Post data
return render(request,'some-template.html',{'form': form})
else:
form = MyForm()
return render(request, 'some-template.html',{'form': form})

Categories