KeyError from customized clean() method in BaseModelFormset - python

I have read over the Forms and Formset Django documentation about 100x. To make this very clear, this is probably the first time I've ever used super() or tried to overload/inherit from another class (big deal for me.)
What's happening? I am making a django-model-formset in a view and I am passing it to a template. The model that the formset is inheriting from happens to be a ManyToMany relationship. I want these relationships to be unique, so that if my user is creating a form and they accidentally choose the same Object for the ManyToMany, I want it to fail validation.
I believe I have written this custom "BaseModelFormSet" properly (via the documentation) but I am getting a KeyError. It's telling me that it cannot find cleaned_data['tech'] and I am getting the KeyError on the word 'tech' on the line where I commented below.
The Model:
class Tech_Onsite(models.Model):
tech = models.ForeignKey(User)
ticket = models.ForeignKey(Ticket)
in_time = models.DateTimeField(blank=False)
out_time = models.DateTimeField(blank=False)
def total_time(self):
return self.out_time - self.in_time
The customized BaseModelFormSet:
from django.forms.models import BaseModelFormSet
from django.core.exceptions import ValidationError
class BaseTechOnsiteFormset(BaseModelFormSet):
def clean(self):
""" Checks to make sure there are unique techs present """
super(BaseTechOnsiteFormset, self).clean()
if any(self.errors):
# Don't bother validating enless the rest of the form is valid
return
techs_present = []
for form in self.forms:
tech = form.cleaned_data['tech'] ## KeyError: 'tech' <-
if tech in techs_present:
raise ValidationError("You cannot input multiple times for the same technician. Please make sure you did not select the same technician twice.")
techs_present.append(tech)
The View: (Summary)
## I am instantiating my view with POST data:
tech_onsite_form = tech_onsite_formset(request.POST, request.FILES)
## I am receiving an error when the script reaches:
if tech_onsite_form.is_valid():
## blah blah blah..

Isn't the clean method missing a return statement ? If I remember correctly it should always return the cleaned_data. Also the super call returns the cleaned_data so you should assign it there.
def clean(self):
cleaned_data = super(BaseTechOnsiteFormset, self).clean()
# use cleaned_data from here to validate your form
return cleaned_data
See: the django docs for more information

I used the Django shell to call the forms manually. I found that I was executing the clean() method on all of the forms returned from the view. There were 2 filled out with data, and 2 blank. When my clean() method was iterating through them all, it returned a KeyError when it got to the first blank one.
I fixed my issue by using a try-statement and passing on KeyErrors.

Related

Accessing a hyperlinkedRelatedField object from a permission class

I am trying to make an api backend of something like reddit. I want to ensure that whoever is creating a post (model Post) within a particular subreddit is a member of that subreddit (subreddit model is Sub). Here is my latest effort, which works but seems pretty sloppy, and the serializer for some context.
Post permissions.py
class IsMemberOfSubOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
elif request.data:
# prevent creation unless user is member of the sub
post_sub_pk = get_pk_from_link(request.data['sub'])
user = request.user
user_sub_pks = [sub.pk for sub in user.subs.all()]
if not (post_sub_pk in user_sub_pks):
return False
return True
Post serializers.py
from .models import Post
from redditors.models import User
from subs.models import Sub
class PostSerializer(serializers.HyperlinkedModelSerializer):
poster = serializers.HyperlinkedRelatedField(
view_name='user-detail',
#queryset=User.objects.all(),
read_only=True
)
sub = serializers.HyperlinkedRelatedField(
view_name='sub-detail',
queryset=Sub.objects.all()
)
class Meta:
model = Post
fields = ('url', 'id', 'created', 'updated', 'title', 'body',
'upvotes', 'sub', 'poster')
The issue with this approach is that since 'sub' is a hyperlinkedRelatedField on the Post serializer what I get back from request.data['sub'] is just the string hyperlink url. I then have a function, get_pk_from_link that uses regex to read the pk off the end of the url. Then I can use that to grab the actual model I want and check things. It would be nice if there were a more direct way to access the Sub model that is involved in the request.
I have tried searching the fields of the arguments that are available and I can't find a way to get to the Sub object directly. Is there a way to access the Sub model object through the hyperlink url?
I have also solved this problem by just using a serializer field validator (not shown above), but I am interested to know how to do it this way too. Maybe this is just a bad idea and if so please let me know why.
You are right, parsing the url is not the way to go. Since you want to perform the permission check before creating a Post object, I suspect you cannot use object level permissions either, because DRF does not call get_object in a CreateAPIView (since the object does not exist in the database yet).
Considering this is a "business logic" check, a simpler approach would be to not have that permission class at all and perform the check in your perform_create hook in your view (I had asked a similar question about this earlier):
from rest_framework.exceptions import PermissionDenied
# assuming you have a view class like this one for creating Post objects
class PostList(generics.CreateApiView):
# ... other view stuff
def perform_create(self, serializer):
sub = serializer.get('sub') # serializer is already validated so the sub object exists
if not self.request.user.subs.filter(pk=sub.pk).exists():
raise PermissionDenied(detail='Sorry, you are not a member of this sub.')
serializer.save()
This saves you hassle of having to perform that url parsing as the serializer should give you the Sub object directly.

using a ModelForm to sanitize post data in Django

I've come across How to create object from QueryDict in django? , which answers what I want to do. However I want to sanitize the data. What does the Brandon mean by "using a ModelForm" to sanitize posted data?
ModelForm are very helpful when you want to create just model instances. If you create a form that closely looks like a model then you should go for a model form instead. Here is an example.
Going by the example provided in the Django website.
In your forms.py
class ArticleForm(ModelForm):
class Meta:
model = Articels #You need to mention the model name for which you want to create the form
fields = ['content', 'headline'] #Fields you want your form to display
So in the form itself you can sanitize your data as well. There are 2 ways of doing that.
Way 1: Using the clean function provided by Django using which you can sanitize all your fields in one function.
class ArticleForm(ModelForm):
class Meta:
model = Articels #You need to mention the model name for which you want to create the form
fields = ['content', 'headline'] #Fields you want your form to display
def clean(self):
# Put your logic here to clean data
Way 2: Using clean_fieldname function using which you can clean your form data for each field separately.
class ArticleForm(ModelForm):
class Meta:
model = Articels #You need to mention the model name for which you want to create the form
fields = ['content', 'headline'] #Fields you want your form to display
def clean_content(self):
# Put your logic here to clean content
def clean_headline(self):
# Put your logic here to clean headline
Basically you would use clean and clean_fieldname methods to validate your form. This is done to raise any error in forms if a wrong input is submitted. Let's assume you want the article's content to have at least 10 characters. You would add this constraint to clean_content.
class ArticleForm(ModelForm):
class Meta:
model = Articels #You need to mention the model name for which you want to create the form
fields = ['content', 'headline'] #Fields you want your form to display
def clean_content(self):
# Get the value entered by user using cleaned_data dictionary
data_content = self.cleaned_data.get('content')
# Raise error if length of content is less than 10
if len(data_content) < 10:
raise forms.ValidationError("Content should be min. 10 characters long")
return data_content
So here's the flow:
Step 1: User open the page say /home/, and you show the user a form to add new article.
Step 2: User submits the form (content length is less than 10).
Step 3: You create an instance of the form using the POST data. Like this form = ArticleForm(request.POST).
Step 4: Now you call the is_valid method on the form to check if its valid.
Step 5: Now the clean_content comes in play. When you call is_valid, it will check if the content entered by user is min. 10 characters or not. If not it will raise an error.
This is how you can validate your form.
What he mean is that with ModelForm you can not only create model instance from QueryDict, but also do a bunch of validation on data types and it's requirements as for example if value's length correct, if it's required etc. Also you will pass only needed data from QueryDict to model instance and not whole request
So typical flow for this is:
form = ModelForm(request.POST)
if form.is_valid():
form.save()
return HttpResponse('blah-blah success message')
else:
form = ModelForm()
return HttpResponse('blah-blah error message')
And awesome Django docs for this: https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#django.forms.ModelForm

how to ensure that only my custom clean_field is called when a form is submitted?

I have a django form like this :
class PatientForm(forms.Form):
patient_id = forms.IntegerField()
patient_national_code = forms.CharField()
and I have a custom clean_ method for this form:
def clean_patient_national_code(self):
patient_national_code = self.cleaned_data['patient_national_code']
if not patient_national_code:
raise forms.ValidationError("My Error!")
return patient_national_code
but when I try to submit a form that its national_code field is empty(and should return "MY Error!"), it returns "This field is required." error. I think that it is the error that django's default validator returns, what should I do to get "My Error!" instead of django's default error?
Add required=False in the field definition, in that case django will not perform the check and will call your clean method.
However, note than when this field is not in the form data, accessing it as self.cleaned_data['patient_national_code'] may result in KeyError.

Enhance is_valid() with an exception

I have a django charField that is checked via the is_valid() method. The user is supposed to enter a valid logical expression in this field, so I wrote a parsing method that raises an exception if the expression is not correct.
How can I enhance the is_valid() method to cover this exception and display an error message to the user that his query was wrong?
I read this article (https://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-a-specific-field-attribute) but still have no idea how to do that.
try:
job = Job(user=request.user) # set the current user
form = JobForm(request.POST, instance=job)
if form.is_valid():
form.save()
job.execute()
messages.success(request, u'A new job with the query "{query}" was created.'.format(query=job.query))
return HttpResponseRedirect(reverse('job-index'))
return self.render_to_response({'job_form': form, 'is_new': True})
except ParseError:
return self.render_to_response({'job_form': form, 'is_new': True})
The try...except-Block should be done within the is_valid() method, that is my intention. Someone got any hints?
You've provided an answer to the question yourself - you create your own form (or model form) and perform custom validation on that form's field using its clean_'fieldname'() method. So for example, say your model is:
class Job(models.Model):
expression_field = models.CharField(...)
...
you create a forms.py:
class JobForm(forms.ModelForm):
pass
class Meta:
model = Job
def clean_expression_field(self):
# You perform your custom validation on this field in here,
# raising any problems
value = self.cleaned_data['expression_field']
if value is 'really_bad':
raise forms.ValidationError("bad bad bad")
return value
then make use of it in your views.py as you already are in your example. Now if the value the user enters doesn't meet your criteria, an exception will be automatically raised

How do I raise a ValidationError (or do something similar) in views.py of my Django?

I'm using Django forms. I'm validating in the model layer:
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 5:
raise forms.ValidationError("Headline must be more than 5 characters.")
return title
However, there are some things that I need to validate in the views.py . For example...was the last time the user posted something more than a minute ago?
That kind of stuff requires request.user, which the models layer cannot get. So, I must validate in the views.py. How do I do something in the views.py to do the exact thing as this?
raise forms.ValidationError("Headline must be more than 5 characters.")
I think gruszczy's answer is a good one, but if you're after generic validation involving variables that you think are only available in the view, here's an alternative: pass in the vars as arguments to the form and deal with them in the form's main clean() method.
The difference/advantage here is that your view stays simpler and all things related to the form content being acceptable happen in the form.
eg:
# IN YOUR VIEW
# pass request.user as a keyword argument to the form
myform = MyForm(user=request.user)
# IN YOUR forms.py
# at the top:
from myapp.foo.bar import ok_to_post # some abstracted utility you write to rate-limit posting
# and in your particular Form definition
class MyForm(forms.Form)
... your fields here ...
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user') # cache the user object you pass in
super(MyForm, self).__init__(*args, **kwargs) # and carry on to init the form
def clean(self):
# test the rate limit by passing in the cached user object
if not ok_to_post(self.user): # use your throttling utility here
raise forms.ValidationError("You cannot post more than once every x minutes")
return self.cleaned_data # never forget this! ;o)
Note that raising a generic ValidationError in the clean() method will put the error into myform.non_field_errors so you'll have to make sure that your template contains {{form.non_field_errors}} if you're manually displaying your form
You don't use ValidationError in views, as those exceptions as for forms. Rather, you should redirect the user to some other url, that will explain to him, that he cannot post again that soon. This is the proper way to handle this stuff. ValidationError should be raised inside a Form instance, when input data doesn't validate. This is not the case.
You can use messages in views:
from django.contrib import messages
messages.error(request, "Error!")
Documentation: https://docs.djangoproject.com/es/1.9/ref/contrib/messages/

Categories