How to deal with errors in large formsets? - python

I'm just starting with formsets and I have trouble making a user friendly error system.
So the user sees a list view that shows him all instances of a model already written into a formset. This pages is meant to show him the data and edit it as well.
Before I was using a lot of individual forms with a save button for every form. But now I want to improve it to have a formset that provides a single save button for all of the forms.
But there comes my problem: Before I used to send the user via "action" to another url (e.g. .../update/ ) which processes the request and then sends him back to the list view he saw before. That's to avoid multiple submits when hitting F5.
But now if I do this and only a single form is wrong all the information the user had entered is lost.
So instead I stopped using the extra URL and made the processing part of the list view. Now I can use form.error on every form, but also the user resubmits when hitting F5.
Is there a way to have both?
Also: I have 1 extra form. But if the user changes it, and I feed the POST data into the formset, save it and then put it back to the page I lost the extra formset, because now the former extra is showing the newly entered instance and there is no true extra field left until the page is refreshed without sending post data.
Here is my View:
class BVServerListView(View):
def get(self, request):
eigene_server = Server.objects.filter(user=request.user)
EigeneServerFormset = modelformset_factory(Server, extra=1, form=ServerForm)
eigene_server_formset = EigeneServerFormset(queryset=eigene_server)
context = {'eigene_server': eigene_server_formset,}
return render(request, 'bildverteiler/server_list.html', context)
def post(self, request):
eigene_server = Server.objects.filter(user=request.user)
EigeneServerFormset = modelformset_factory(Server, extra=1, form=ServerForm)
eigene_server_formset = EigeneServerFormset(request.POST, request.FILES)
for form in eigene_server_formset.forms:
if form.data.get('delete', False):
server = Server.objects.get(user=request.user, name=form.data['name'])
server.delete()
else:
if form.has_changed() and form.is_valid():
server = form.save(commit=False)
server.user = request.user
server.save()
context = {'eigene_server': eigene_server_formset,}
return render(request, 'bildverteiler/server_list.html', context)

There is no difference between using a single form or a formset here. The answer is the same: post to the same view, but redirect after a successful save.
The other thing that you are doing wrong here is to validate and save individual forms one by one. Don't do that, because you could end up with a situation that the first forms are valid and get saved, but subsequent ones are invalid and therefore the formset needs to be redisplayed. Instead, validate the formset as a whole:
if eigene_server_formset.is_valid():
for form in eigene_server_formset.forms:
if form.cleaned_data.get('delete'):
... delete ...
else:
form.save()
return HttpResponseRedirect('somewhere_else')
return render...

Related

Django Redirect with non-default table

In my Django project, I have a generic view. It has a single select form in which the user can choose a formset from a list of formsets to load.
Then, a POST request is sent to the server, requesting the new formset. The view is reloaded with the new formset while remaining on the same URL.
Here's an example:
Assume there are two tables: default_formset and formset_2
If I am making updates on my default_formset the redirect works perfectly. The issue I am having is with 'updating' non-default formsets like formset_2.
When an update is made on formset_2 on redirect it will load up the default_formset rather than loading up formset_2
In foo.py:
def generic_view(request):
#Here, the user's input determines what formset is rendered
formset_names = SingleSelectForm(formsets, 'formset_name', request.POST)
#This variable gets the user's input
formset_selected = request.POST.get('formset_name', 'default_formset')
''' '''
Forms are initialized
''' '''
if request.POST:
if 'formset_name' in request.POST:
formset = the_formset(form_kwargs=empty_forms, initial=initial_values)
if 'update' in request.POST:
if formset.is_valid():
for form in formset:
if form.has_changed():
# Do some mysql stuff
return HttpResponseRedirect("/foo/")
In urls.py:
from website.foo import foo
urlpatterns = [
path('foo/', foo.generic_view, name='generic_view'),
]
If a user updated a row in formset_2, is it possible to reload this generic_view with the updated formset_2 instead of default_formset?

Django: User Reporting Some URL on Website

So i'm trying to build something, so that users would be able to report something on site. Here's the model,
class Report(models.Model):
reporting_url = models.URLField()
message = models.TextField()
I created Form for this Model including 'message' field only because 'reporting_url' is something it needs to populate by itself depending upon the specific page from where user has clicked "Report" button.
def report(request):
url_report = ???
if request.method == 'POST':
form = ReportForm(request.POST or None)
if form.is_valid():
new_form = form.save(commit=False)
new_form.reporting_url = url_report
new_form.save()
I was wondering How can I pass the specific url to 'reporting_url' field in form depending on the Page from where user has clicked "Report" button? (Much like s we see on social Networks).
Am I doing this correctly, Or is there a better way for doing this?
Please help me with this code. Thanks in Advance!
If there is a report button on that specific page then I believe you could write custom context processor.
More info: Django: get URL of current page, including parameters, in a template
https://docs.djangoproject.com/en/1.11/ref/templates/api/
Or maybe just write it directly in the views.py in your function and set
url_report = request.get_full_path()
I think you can use the form on the same page of the URL and use:
url_report = request.get_full_path()
in the view, to get the current URL.
Else if you want to create a separate view for the reporting form. You can use
url_report = request.META.get('HTTP_REFERER')
to get the previous or refering URL which led the user to that page.
request.META.get('HTTP_REFERER') will return None if it come from a different website.

Prevent repopulation and/or resubmit of Django form after using the back button

The Problem
We have the following setup.
Pretty standard Django class based view (inherits from CreateView, which is what I'll call it form now on).
After a successful POST and form validation, the object is created, and the user is redirect_to'd the DetailView of the created record.
Some users decide that they are not happy with the data they entered. They press the back button.
The HTML generated by the CreateView is fetched form browser cache, and repopulated with the data they entered.
To the user, this feels like an edit, so they change the data and submit again.
The result is 2 records, with minor differences.
What have we tried?
At first I thought the Post-Redirect-Get (PRG) pattern that Django uses was supposed to prevent this. After investigating, it seems that PRG is only meant to prevent the dreaded "Do you want to resubmit the form?" dialog. Dead end.
After hitting the back button, everything is fetched from cache, so we have no chance of interacting with the user from our Django code. To try and prevent local caching, we have decorated the CreateView with #never_cache. This does nothing for us, the page is still retrieved form cache.
What are we considering?
We are considering dirty JavaScript tricks that do an onLoad check of window.referrer, and a manual clean of the form and/or notice to user if the referrer looks like the DetailView mentioned earlier. Of course this feel totally wrong. Then again, so do semi-duplicate records in our DB.
However, it seems so unlikely that we are the first to be bothered by this that I wanted to ask around here on StackOverflow.
Ideally, we would tell the browser that caching the form is a big NO, and the browser would listen. Again, we already use #never_cache, but apparently this is not enough. Happens in Chrome, Safari and Firefox.
Looking forward to any insights! Thanks!
Maybe don't process the POST request when it's coming from a referrer other than the same page?
from urllib import parse
class CreateView(...):
def post(self, *args, **kwargs):
referer = 'HTTP_REFERER' in self.request.META and parse.urlparse(self.request.META['HTTP_REFERER'])
if referer and (referer.netloc != self.request.META.get('HTTP_HOST') or referer.path != self.request.META.get('PATH_INFO')):
return self.get(*args, **kwargs)
...
I know I'm late to this party but this may help anybody else looking for an answer.
Having found this while tearing my hair out over the same problem, here is my solution using human factors rather than technical ones. The user won't use the back button if after submitting from a CreateView, he ends up in an UpdateView of the newly created object that looks exactly the same apart from the title and the buttons at the bottom.
A technical solution might be to create a model field to hold a UUID and create a UUID passed into the create form as a hidden field. When submit is pressed, form_valid could check in the DB for an object with that UUID and refuse to create what would be a duplicate (unique=True would enforce that at DB level).
Here's example code (slightly redacted to remove stuff my employer might not want in public). It uses django-crispy-forms to make things pretty and easy. The Create view is entered from a button on a table of customers which passes the customer account number, not the Django id of its record.
Urls
url(r'enter/(?P<customer>[-\w]+)/$', JobEntryView.as_view(), name='job_entry'),
url(r'update1/(?P<pk>\d+)/$', JobEntryUpdateView.as_view(), name='entry_update'),
Views
class JobEntryView( LoginRequiredMixin, CreateView):
model=Job
form_class=JobEntryForm
template_name='utils/generic_crispy_form.html' # basically just {% crispy form %}
def get_form( self, form_class=None):
self.customer = get_object_or_404(
Customer, account = self.kwargs.get('customer','?') )
self.crispy_title = f"Create job for {self.customer.account} ({self.customer.fullname})"
return super().get_form( form_class)
def form_valid( self, form): # insert created_by'class
#form.instance.entered_by = self.request.user
form.instance.customer = self.customer
return super().form_valid(form)
def get_success_url( self):
return reverse( 'jobs:entry_update', kwargs={'pk':self.object.pk, } )
# redirect to this after entry ... user hopefully won't use back because it's here already
class JobEntryUpdateView( LoginRequiredMixin, CrispyCMVPlugin, UpdateView):
model=Job
form_class=JobEntryForm
template_name='utils/generic_crispy_form.html'
def get_form( self, form_class=None):
self.customer = self.object.customer
self.crispy_title = f"Update job {self.object.jobno} for {self.object.customer.account} ({self.object.customer.fullname})"
form = super().get_form( form_class)
form.helper[-1] = ButtonHolder( Submit('update', 'Update', ), Submit('done', 'Done', ), )
return form
def get_success_url( self):
print( self.request.POST )
if self.request.POST.get('done',None):
return reverse('jobs:ok')
return reverse( 'jobs:entry_update',
kwargs={'pk':self.object.pk, } ) # loop until user clicks Done

Django file field update causing error even though not required

I have an app that serves to update certain fields of a model. There are 4 possible fields that could be updated: resolution, upload4, upload5, and upload6. The upload fields are NOT required. If I do not include the request.FILES line, the uploaded file will not be saved to the database, but it seems like because I've included it, I need to always upload the 3 files, even though they are not required. The exception I am getting is "MultiValueDictKeyError" on the POST. How can I fix this? I want the option to add 3 files, but I don't want to have to every time. I understand how to make a field not required, I don't know how to code the request.FILES to understand that it is not required.
views.py
#login_required(login_url='/login/')
def report(request, case_id):
form = ReportForm()
case = get_object_or_404(Incident, pk=case_id)
# if this is a POST request we need to process the form data
if request.POST:
# create a form instance and populate it with the data from the request:
form = ReportForm(request.POST)
if form.is_valid():
resolution = (form.cleaned_data['resolution']) # grabbing action_taken from user input
case.resolution = resolution
case.upload4 = request.FILES['upload4']
case.upload5 = request.FILES['upload5']
case.upload6 = request.FILES['upload6']
case.status = Status.objects.get(status='closed')
case.save(update_fields=['resolution', 'status', 'upload4', 'upload5', 'upload6'])
context = { 'case': case,
'form': form}
return HttpResponseRedirect(reverse('dashboard_app:dashboard'))
template = "report.html"
#form = CaseForm()
context = { 'case': case,
'form': form}
return render(request, template, context)
The point is that you are ignoring the validation that form does, and going straight back to the data from the request. So, yes, that will break if the forms are not there. But this is exactly why we use forms.
case.upload4 = form.cleaned_data['upload4']
etc.
It would be even easier if you used a ModelForm; then you could pass case as the instance argument of the form, and just do form.save(), replacing almost all the code inside your is_valid block.
This will solve the problem.
case.upload4 = request.FILES.get('upload4')
case.upload5 = request.FILES.get('upload5')
case.upload6 = request.FILES.get('upload6')

Django form returns is_valid() = False and no errors

I have simple view in django app, which I want to show only when one of the forms is valid. I have something like:
#login_required
#require_role('admin')
def new_package(request):
invoicing_data_form = InvoicingDataForm(instance=request.user.account.company.invoicingdata)
if invoicing_data_form.is_valid():
# all here
return HttpResponse('Form valid')
else:
logger.info("Form invalid")
return HttpResponse(json.dumps(invoicing_data_form.errors)
I always get log info message that form is invalid, however, I get nothing in
invoicing_data_form.errors
It is very strange, because I am validating this form in other view using user input data and it works just fine. Any idea?
EDIT:
Just for clarification.
I am not requesting any data from user in this form.
I am using this form to validate some model instance (this form is subclassing from ModelForm).
That's because you're not "feeding" your form.
Do this:
invoicing_data_form = InvoicingDataForm(instance=invoice, data=request.POST or None)
You have an unbound form.
https://docs.djangoproject.com/en/1.7/ref/forms/api/#bound-and-unbound-forms
A Form instance is either bound to a set of data, or unbound.
If it’s bound to a set of data, it’s capable of validating that data and rendering the form as HTML with the data displayed in the HTML.
If it’s unbound, it cannot do validation (because there’s no data to validate!), but it can still render the blank form as HTML.
To bind data to a form, pass the data as a dictionary as the first parameter to your Form class constructor:
invoicing_data_form = InvoicingDataForm(request.POST or None, instance=invoice)
If you're already giving request.POST to your form using request.POST or None, but it's still invalid without errors, check that there isn't any redirect going on. A redirect loses your POST data and your form will be invalid with no errors because it's unbound.
I got this for AuthenticationForm which needs AuthenticationForm(None, request.POST) see Using AuthenticationForm in Django
I want to expand on the answer by #yuji-tomita-tomita
I typically use a CBV approach in Django, and how I'm handling forms:
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
# do things
Reading the source code I noticed that self.get_form() using get_form_kwargs(self) to populate the form with request.POST, thus getting bound to data. So if you're overloading it like I did:
def get_form_kwargs(self):
company = self.get_company()
return {"company": company}
Make sure to call the super(), and it will finally work:
def get_form_kwargs(self):
company = self.get_company()
kwargs = super().get_form_kwargs()
kwargs.update({"company": company})
return kwargs

Categories