Django custom form validation best practices? - django

I have a form that contains 5 pairs of locations and descriptions. I have three sets of validations that need to be done
you need to enter at least one location
for the first location, you must have a description
for each remaining pair of locations and description
After reading the Django documentation, I came up with the following code to do these custom validations
def clean(self):
cleaned_data = self.cleaned_data
location1 = cleaned_data.get('location1')
location2 = cleaned_data.get('location2')
location3 = cleaned_data.get('location3')
location4 = cleaned_data.get('location4')
location5 = cleaned_data.get('location5')
description1 = cleaned_data.get('description1')
description2 = cleaned_data.get('description2')
description3 = cleaned_data.get('description3')
description4 = cleaned_data.get('description4')
description5 = cleaned_data.get('description5')
invalid_pairs_msg = u"You must specify a location and description"
# We need to make sure that we have pairs of locations and descriptions
if not location1:
self._errors['location1'] = ErrorList([u"At least one location is required"])
if location1 and not description1:
self._errors['description1'] = ErrorList([u"Description for this location required"])
if (description2 and not location2) or (location2 and not description2):
self._errors['description2'] = ErrorList([invalid_pairs_msg])
if (description3 and not location3) or (location3 and not description3):
self._errors['description3'] = ErrorList([invalid_pairs_msg])
if (description4 and not location4) or (location4 and not description4):
self._errors['description4'] = ErrorList([invalid_pairs_msg])
if (description5 and not location5) or (location5 and not description5):
self._errors['description5'] = ErrorList([invalid_pairs_msg])
return cleaned_data
Now, it works but it looks really ugly. I'm looking for a more "Pythonic" and "Djangoist"(?) way to do this. Thanks in advance.

First thing you can do is simplify your testing for those cases where you want to see if only one of the two fields is populated. You can implement logical xor this way:
if bool(description2) != bool(location2):
or this way:
if bool(description2) ^ bool(location2):
I also think this would be more clear if you implemented a clean method for each field separately, as explained in the docs. This makes sure the error will show up on the right field and lets you just raise a forms.ValidationError rather than accessing the _errors object directly.
For example:
def _require_together(self, field1, field2):
a = self.cleaned_data.get(field1)
b = self.cleaned_data.get(field2)
if bool(a) ^ bool(b):
raise forms.ValidationError(u'You must specify a location and description')
return a
# use clean_description1 rather than clean_location1 since
# we want the error to be on description1
def clean_description1(self):
return _require_together('description1', 'location1')
def clean_description2(self):
return _require_together('description2', 'location2')
def clean_description3(self):
return _require_together('description3', 'location3')
def clean_description4(self):
return _require_together('description4', 'location4')
def clean_description5(self):
return _require_together('description5', 'location5')
In order to get the behavior where location1 is required, just use required=True for that field and it'll be handled automatically.

At least you can reduce some code. Have 'location1' and 'description1' Required=True (as TM and stefanw pointed out). Then,
def clean(self):
n=5
data = self.cleaned_data
l_d = [(data.get('location'+i),data.get('description'+i)) for i in xrange(1,n+1)]
invalid_pairs_msg = u"You must specify a location and description"
for i in xrange(1,n+1):
if (l_d[i][1] and not l_d[i][0]) or (l_d[i][0] and not l_d[i][1]):
self._errors['description'+i] = ErrorList([invalid_pairs_msg])
return data
still ugly though...

Related

Form fields are empty when created using instance = instance

I have two models prodcut_prices and WrongPrice.
In WrongPrice the user can correct report wrong prices - when the price is reported it should also be updated in product_price.
My problem is, even though I instantiate product_price at the very beginning as instance_productprice, all of its required fields returns the "this field has to be filled out" error.
How come those field are not set when im using the instance instance_productprice = product_prices.objects.filter(id=pk)[0] ? Note, that all fields in product_prices are always non-empty since they are being pulled from the product_price model, which is handled in another view, thus that is not the issue.
def wrong_price(request,pk):
#Get the current price object
instance_productprice = product_prices.objects.filter(id=pk)[0]
#Get different values
wrong_link = instance_productprice.link
img_url = instance_productprice.image_url
wrong_price = instance_productprice.last_price
domain = instance_productprice.domain
# Create instances
instance_wrongprice = WrongPrice(
link=wrong_link,
correct_price=wrong_price,
domain = domain)
if request.method == "POST":
form_wrong_price = wrong_price_form(request.POST,instance=instance_wrongprice)
# Update values in product_prices
form_product_price = product_prices_form(request.POST,instance=instance_productprice)
form_product_price.instance.start_price = form_wrong_price.instance.correct_price
form_product_price.instance.last_price = form_wrong_price.instance.correct_price
if form_wrong_price.is_valid() & form_product_price.is_valid():
form_wrong_price.save()
form_product_price.save()
messages.success(request, "Thanks")
return redirect("my_page")
else:
messages.error(request, form_product_price.errors) # Throws empty-field errors,
messages.error(request, form_wrong_price.errors)
return redirect("wrong-price", pk=pk)
else:
form_wrong_price = wrong_price_form(instance=instance_wrongprice)
return render(request, "my_app/wrong_price.html",context={"form":form_wrong_price,"info":{"image_url":img_url}})
I am bit confused about how you implemented it. You have passed a instance of WrongPrice price through the form, which is unnecessary, you could have used initial:
wrong_values = dict(
link=wrong_link,
correct_price=wrong_price,
domain = domain
)
form_wrong_price = wrong_price_form(initial= wrong_values)
Then you are adding values to product_prices_form from instance of form_wrong_price. I don't see why you need a form again here. You can simple use:
form_wrong_price = wrong_price_form(request.POST, initial= wrong_values)
if form_wrong_price.is_valid():
instance = form_wrong_price.save()
instance_productprice.start_price = instance.correct_price
instance_productprice.last_price = instance.correct_price
instance_productprice.save()
Finally, please use PascalCase when defining class names. And you can get the product prices by product_prices.objects.get(id=pk)(instead of filter()[0]).

Django inline-formset ordering issue when editing

I am trying to use the Django inline-formset.
Forms should be displayed sorted by their order value which is correctly done when I request the form.
But if I change the order values and save, the first view is with the previous order (refresh does the trick)
forms:
class SlidesForm(forms.ModelForm):
order = forms.IntegerField(widget=forms.NumberInput())
background_image = forms.ImageField(widget=forms.FileInput(attrs={'class': 'custom-file-input'}), required=False)
text = forms.CharField(max_length=256, widget=forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}), required=False)
class Meta:
model = SlideCarousel
fields = ['order', 'background_image', 'text']
views:
def management_form_general(request, city_slug):
city = City.objects.get(slug=city_slug)
SlideCarouselInlineFormSet = inlineformset_factory(City, SlideCarousel, form=SlidesForm, extra=0)
if request.method == 'POST':
carousel_formset = SlideCarouselInlineFormSet(request.POST, request.FILES, instance=city, queryset=city.slidecarousel_set.order_by("order"))
if carousel_formset.is_valid():
carousel_formset.save()
else:
carousel_formset = SlideCarouselInlineFormSet(instance=city, queryset=city.slidecarousel_set.order_by("order"))
return render(request, 'management/form/city_general.html', {'city': city, 'carousel_formset': carousel_formset})
Any idea what I am doing wrong ? Tried to reinstance the carousel_formset after the save but it seems nasty and it actually didn't work
Right now you're still returning the same queryset (already evaluated and ordered) in the formset. What you need is to get the data that you just saved and update the formset with it. I think that you have two options that should work.
Recreate the carousel_formset like you said. This might not be exactly what you want but it seems more likely than my second suggestion. You said you tried this and it didn't work. If your code looks the same as mine then you might want to skip this approach.
carousel_formset.save()
carousel_formset = SlideCarouselInlineFormSet(
instance=city,
queryset=city.slidecarousel_set.order_by("order"),
)
Usually, after I save a form(set) I would redirect to a success URL. In this case that would be the same path again.
carousel_formset.save()
return redirect(request.path)
A third option that I have no idea if it will work, but you could try for very little effort, would be to re-set the carousel_formset.queryset attribute.
carousel_formset.save()
carousel_formset.queryset = city.slidecarousel_set.order_by("order")

How to check which the fields of a form has been modified in Flask?

I am trying to figure out a way to check if there is any built-in Form method that would return true if the form has been modified in Flask/WTForms
I know that in Django Forms, we have that flexibility to use form.has_changed() method that would do exactly what i want.
I am trying to check if the form has been modified and if it is I would to do some database updates.
If anybody has any idea, please let me know about this or suggest the right approach to start with.
I haven't found any built-in Form methods, but here is my quirky approach how I check whether the form was changed, and which fields were changed.I'm just storing the whole original form in the additional StringField, and then comparing it with the new form. As I don't want this field to be displayed on the page, I'm changing it's style to style="visibility:hidden;display:none" in the html file
#app.route("/page", methods=["GET", "POST"])
def page(tag=None):
class Person(FlaskForm):
name = StringField("name")
age = StringField("name")
class Spouses(FlaskForm):
people = FieldList(FormField(JetsonForm), min_entries=0)
people_orig = StringField("people_orig")
submit = SubmitField("Submit")
data_from_database = get_data_from_database()
data = {'people': [], 'people_orig': json.dumps(data_from_database)}
for person_name in data_from_database:
data['people'].append(data_from_database[person_name])
form = Spouses(data=data)
people_orig_data = json.loads(form.people_orig.data)
people_new_data = dict()
for person in form.people:
people_new_data[person.name.data] = {
"name": persom.name.data,
"age": peron.age.data
}
if form.is_submitted():
for person_name in people_new_data:
for field in people_new_data[person_name]:
if people_new_data[person_name][field] != people_orig_data[cam][key]:
#update your database
return redirect(request.url)
return render_template("page.html",form=form)

Set default value for dynamic choice field

I have a form that asks the user to enter in their zip code. Once they do it sends them to another form where there is a field called 'pickup_date'. This gets the value of the zip from the previous field and gets all of the available pickup_dates that match that zip code into a ChoiceField. I set all of this within the init of the model form.
def __init__(self,*args,**kwargs):
super(ExternalDonateForm,self).__init__(*args,**kwargs)
if kwargs:
zip = kwargs['initial']['zip']
self.fields['pickup_date'] = forms.ChoiceField(choices = self.get_dates(zip))
elif self.errors:
zip = self.data['zip']
self.fields['pickup_date'] = forms.ChoiceField(choices = self.get_dates(zip))
The problem I have is when there are other errors on the form. I use the elif self.errors to regenerate the possible choices but it doesn't default to the original selected option. It goes back and defaults to the first choice. How can I make it so it's default option on form errors is what was originally posted?
Change self.fields['pickup_date'] to self.fields['pickup_date'].initial and see if that helps.
I got it to work after playing around for a while. Above, I was setting all the dynamic choices with a get_dates() function that returned a tuple. Instead of doing that I returned a field object like this using a customized ModelChoiceField instead of a regular ChoiceField....
class MyModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.date.strftime('%a %b %d, %Y')
Dates function
def get_dates(self,zip):
routes = Route.objects.filter(zip=zip).values_list('route',flat=True)
pickups = self.MyModelChoiceField(queryset = PickupSchedule.objects.filter(
current_count__lt=F('specials'),
route__in=routes,
).order_by('date')
)
if not pickups:
pickups = (('----','No Pickups Available At This Time'),)
return pickups
in the init i set the value for self.fields['pickup_date'] like so..
self.fields['pickup_date'] = self.get_dates(zip)

Django: Overriding the save() method: how do I call the delete() method of a child class

The setup =
I have this class, Transcript:
class Transcript(models.Model):
body = models.TextField('Body')
doPagination = models.BooleanField('Paginate')
numPages = models.PositiveIntegerField('Number of Pages')
and this class, TranscriptPages(models.Model):
class TranscriptPages(models.Model):
transcript = models.ForeignKey(Transcript)
order = models.PositiveIntegerField('Order')
content = models.TextField('Page Content', null=True, blank=True)
The Admin behavior I’m trying to create is to let a user populate Transcript.body with the entire contents of a long document and, if they set Transcript.doPagination = True and save the Transcript admin, I will automatically split the body into n Transcript pages.
In the admin, TranscriptPages is a StackedInline of the Transcript Admin.
To do this I’m overridding Transcript’s save method:
def save(self):
if self.doPagination:
#do stuff
super(Transcript, self).save()
else:
super(Transcript, self).save()
The problem =
When Transcript.doPagination is True, I want to manually delete all of the TranscriptPages that reference this Transcript so I can then create them again from scratch.
So, I thought this would work:
#do stuff
TranscriptPages.objects.filter(transcript__id=self.id).delete()
super(Transcript, self).save()
but when I try I get this error:
Exception Type: ValidationError
Exception Value: [u'Select a valid
choice. That choice is not one of the
available choices.']
... and this is the last thing in the stack trace before the exception is raised:
.../django/forms/models.py in save_existing_objects
pk_value = form.fields[pk_name].clean(raw_pk_value)
Other attempts to fix:
t =
self.transcriptpages_set.all().delete()
(where self = Transcript from the
save() method)
looping over t (above) and deleting each item individually
making a post_save signal on TranscriptPages that calls the delete method
Any ideas? How does the Admin do it?
UPDATE: Every once in a while as I'm playing around with the code I can get a different error (below), but then it just goes away and I can't replicate it again... until the next random time.
Exception Type:
MultiValueDictKeyError Exception
Value: "Key 'transcriptpages_set-0-id'
not found in "
Exception Location:
.../django/utils/datastructures.py in
getitem, line 203
and the last lines from the trace:
.../django/forms/models.py in _construct_form
form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs)
.../django/utils/datastructures.py in getitem
pk = self.data[pk_key]
In the end it was a matter of timing. When deleting only a single child object out of many, there was no problem. If I was deleting too many child objects at once, the error could happen, because the delete action was attempting to reference ids that were not around. This is why nothing worked, not signals, not [object]_set. I fixed it by using jquery to set a hidden variable in the edit form for the child object, which caused the object to first process the update (slowing down it's processing) and THEN the delete.
You are probably trying to access the Transaction.id before is has been created. Additionally, you can try to access the TransactionPage objects through the Transaction object via transactionpage_set (see query docs about FOO_set notation).
def save(self):
super(Transcript, self).save()
if self.doPagination:
self.transaction_set.all().delete() # .all() may be optional
pages = ... # whatever you do to split the content
self.numPages = len(pages)
self.save() # yes, a second save if you store numPages in Transaction
for page_number in range(self.numPages):
TransactionPage.objects.create(content=pages[page_number],
order=page_number, transaction=self)
You could also switch to not storing numPages in the Transaction and access it via a property instead.
class Transaction(models.Model):
# ...
# replace numPages with
#property
def page_count(self):
return self.transactionpage_set.count() or 1
And then if you took it one step further you could always use the TransactionPage objects for display purposes. This would allow you to get rid of the extra self.save() call in the above save() method. It will also let you simplify your templates by always displaying TransactionPage.content instead of conditionally displaying Transaction.body if you are paginating and TransactionPage.content otherwise.
class Transaction(models.Model):
body = models.TextField()
paginate = models.BooleanField()
#property
def page_count(self):
return self.transactionpage_set.count() # no more "or 1" cruft!
def save(self):
super(Transcript, self).save()
self.transaction_set.delete() # might need an .all() before .delete()
if self.paginate:
# Do whatever you do to split the body into pages.
pages = ...
else:
# The only page is the entire body.
pages = [self.body]
for (page_number, page_content) in enumerate(pages):
TransactionPage.objects.create(content=page_content,
order=page_number, transaction=self)
Another possible solution might be to override save_formset() in the ModelAdmin to prevent the update completely:
def save_formset(self, request, form, formset, change):
if (form.cleaned_data['do_pagination'] and
formset.model == TranscriptPages):
formset.changed_objects = []
formset.new_objects = []
formset.deleted_objects = []
else:
formset.save()
Then, you can do whatever you like in Modal.save(). Note that there's probably a more elegant/breakproof way to stop the formset from processing if one wanted to dig into the internals a bit more.