I'm trying to learn Django, and I am reading this link right now:
https://docs.djangoproject.com/en/1.5/topics/forms/modelforms/
If you scroll down in the link provided, it says that there are two main steps involved in validating a form and that the first step is 'validating the form' which leads to this link:
https://docs.djangoproject.com/en/1.5/ref/forms/validation/#form-and-field-validation
It says that the first step in every validation is to use the to_python() method on a field. I don't understand what they mean when they say
"It coerces the value to correct datatype and raises ValidationError if that is not possible. This method accepts the raw value from the widget and returns the converted value."
So suppose I have a model like this
class User(models.Model):
user_id = models.AutoField(unique=True, primary_key=True)
username = models.SlugField(max_length=50, unique=True)
first_name = models.CharField(max_length=50)
I created a form like so
class UserForm(forms.ModelForm):
class Meta:
model = User
now, how exactly do I use to_python() method? Do I use it in a view? Or do I have to use it in the forms.py file? If I use it in a view, what would the function be called?
Django does validates and deserializes fields input automatically.
Example view when posting form:
def my_view(request):
form = UserForm()
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid(): # here to_python() is run for each field
form.save()
# redirect
return render_to_response('home.html', { 'form': form })
You don't need to worry about the to_python() unless you are creating a custom field. If you are going to use the ModelForm to create simple forms, you can use the clean methods.
If you want to validate only one field, you can do this:
class UserForm(forms.ModelForm):
def clean_username(self):
username = self.cleaned_data['username']
if len(username) > 10:
raise forms.ValidationError("Please shorten your username")
# Always return the cleaned data, whether you have changed it or
# not.
return username
if you want to clean multiple fields, you can do this:
class Userform(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super(UserForm, self).clean()
username = cleaned_data.get("username")
first_name = cleaned_data.get("first_name")
if len(username) > 10:
raise forms.ValidationError("Please shorten your username")
if len(first_name) < 1:
raise forms.ValidationError("First name is too short")
# Always return the full collection of cleaned data.
return cleaned_data
Related
I'm building a Django project for a client that requires me to not user a simple form.save() method to update a model field.
Basically, it looks like this:
I have this model with a CharField who has unique=True:
# models.py
class Course(models.Model):
name = models.CharField(max_length=20, unique=True)
other_field = models.CharField(max_length=10, null=True)
def __str__(self):
return self.name
That model has a form in forms.py:
# forms.py
class CourseCreateForm(forms.ModelForm):
class Meta:
model = Course
fields = ['name', 'other_field']
I need to update this field through a function view (can't be class based in this scenario. Of course, literally it can, but for my student's project requirements it can't be) and I can't use the simple form.save() function, so I need to do the full update code as if it was a normal form:
# views.py
def course_update(request, pk):
course = Course.objects.get(pk=pk)
course_queryset = Course.objects.filter(pk=pk)
if request.method == "POST":
form = CourseCreateForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
other_field = form.cleaned_data['other_field']
course_queryset.update(name=name, other_field=other_field) # <-- Where I try to update
else:
print(form.errors)
return HttpResponseRedirect('../')
else:
form = CourseCreateForm(instance=course)
context = {
"form": form,
}
return render(request, 'base/course_update.html', context)
When I try to only update the other_field, the change isn't made and in the formerrors I receive the error "Course with this Name already exists.", so I can't only change the other_field data without also having to change the name field because the name field is unique.
How can you update a model instance's field that has unique=True without changing the unique value?
Hope this makes sense!
Thanks.
UPDATE:
Also wanted to add that it works perfectly fine when unique=True is turned off. I'm just curious how do you update a field normally when unique=True and you're adding in the same variable from a model form.
The reason that this will fail is because the ModelForm thinks you are creating a new record, and thus it will check if an object with the given name already exists, and if so, it of course finds out the name already exists, hence the form is not valid.
You can pass the instance to the form. In that case the form will exclude that item from the unique check:
def course_update(request, pk):
course = Course.objects.get(pk=pk)
course_queryset = Course.objects.filter(pk=pk)
if request.method == 'POST':
form = CourseCreateForm(request.POST, instance=course)
if form.is_valid():
name = name_form.cleaned_data['name']
other_field = course_form.cleaned_data['other_field']
course_queryset.update(name=name, other_field=other_field)
return HttpResponseRedirect('../')
else:
print(form.errors)
else:
form = CourseCreateForm(instance=course)
context = {
'name_form': name_form,
'course_form': course_form,
}
return render(request, 'base/course_update.html', context)
I want to write a form that will display the user's email but allow them to change their password by entering their current password and their new password twice. I want to use a ModelForm, too.
I have the User:
# models.py
class User(AbstractBaseUser):
email = models.EmailField(unique=True)
And I've started on a form:
# forms.py
class ChangeAccountDetailsForm(forms.ModelForm):
class Meta:
model = User
fields = ('email',)
current_password = forms.CharField(widget=forms.PasswordInput)
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
I've created a simple view that only works with a GET request at the minute:
# views.py
def edit_account_details(request):
if request.method == 'GET':
account_form = ChangeAccountDetailsForm()
return render(request, 'freelancestudent/edit_account_details.html', {
{'form': account_form}
})
else:
pass
And a simple HTML file that simply calls {{form.as_p}}. When I run it as is I get the error unhashable type: 'dict' at {'form': account_form}. How can I display, then, the current user's email and allow functionality for changing password. Is this possible with a ModelForm or will I have to roll my own form and do all the logic myself?
Here :
return render(
request,
'freelancestudent/edit_account_details.html', {
{'form': account_form}
})
You have two sets of {}. The inner one defines a dict, the outer one defines a set, so it's equivalent to:
context = {'form': account_form}
whatever = set(context)
Now sets needs their values to be hashable (just like dict keys), and a dict is not hashable, hence your error message.
But anyway: render expects a dict, not a set, so what you want is:
return render(
request,
'freelancestudent/edit_account_details.html',
{'form': account_form}
)
Let's say I have the following model:
class Folder(models.Model):
name = models.CharField(default='untitled', max_length=255)
parent = models.ForeignKey('self', null=True, blank=True)
root = models.ForeignKey('self', null=True, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
In my app, this class is used to represents two types of folders: a normal folder-object and a so called root_folder-object, which does not have a parent nor a root-FK set.
I created a custom ModelForm with custom clean(), which is working like a charm (according to unittests and manual testing):
class FolderForm(ModelForm):
def __init__(self, *args, **kwargs):
try:
data = kwargs.get('data', None).copy()
except AttributeError:
data = None
self.prefix = kwargs.get('prefix')
user = kwargs.pop('user', None)
if data is not None:
if user is not None:
data[self.add_prefix('user')] = user.id
kwargs['data'] = data
super(FolderForm, self).__init__(*args, **kwargs)
def clean(self):
# just working fine, so I won't include it here
pass
class Meta:
model = Folder
fields = '__all__'
So, because my root-folder is just a normal Folder-object with blank FKs, I don't want to even show these fields to the user while creation. I created another form for this:
class AddRootFolderForm(FolderForm):
class Meta:
model = Folder
exclude = ['parent', 'root', 'user']
As you can see, I exclude user aswell, this value will be set in the view. Currently, this is my view code:
#login_required
def create_rootfolder(request):
if request.method == 'POST':
form = FolderForm(data = request.POST,
user = request.user)
else:
form = AddRootFolderForm()
if form.is_valid():
new = form.save()
return redirect('show_rootfolder', root_id = new.id)
return render(request, 'create_rootfolder.html',
{ 'form': form })
This whole setup is working, but seems awful hackerish. Is there any better approach to hide certain fields from the user (meaning: Don't even show them as hidden fields), but include them in validation? My main problem is, that I can't use the same form for displaying and validating, because the excluded fields will not be validated, if I use AddRootFolderForm as single form instance.
I am aware that I can exclude the fields dynamically in the constructor, I even tried this, but it got my constructor bloated to 50 LOC, which seemed unclean.
So what would be the best approach to validate the model with all fields, even if they were not included in the form shown to the user?
Why validate fields, not used in Form?
The cleaning process is the way to check the data posted by a user. The rest of the data, required for Model operations must be added after the form validation
if form.is_valid():
new = form.save(commit=False)
new.user = request.user
...
new.save()
Ok so I actually solved this one by accident and would simply like to understand what happened.
I have my own user registration form BaseCreationForm which extends a ModelForm and uses a UserProfile as its model. All the validation methods were working fine, but the save method was giving me grief. Whenever I tried to create a user (the profile is created in the view, I may refactor this), Django would tell me that "BaseCreationForm object has no attribute cleaned data".
BUT, when out of frustration and running out of ideas I added a simple "print self" statement before creating the user in the save() method, the problem disappeared and users are being created normally. Below are a couple of clean() methods that work, the save() method and a snippet from the view that calls the clean() and save() method.
clean() methods working normally
#example clean methods, both work beautifully
def clean_email(self):
email = self.cleaned_data["email"]
if not email:
raise forms.ValidationError(self.error_messages['no_email'])
try:
User.objects.get(email=email)
except User.DoesNotExist:
return email
raise forms.ValidationError(self.error_messages['duplicate_email'])
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
self.error_messages['password_mismatch'])
return password2
save() method:
#save method requiring wizardry
def save(self, commit=True):
#This line makes it work. When commented, the error appears
print self
###
user = User.objects.create_user(
username=self.cleaned_data.get("username"),
first_name=self.cleaned_data["first_name"],
last_name=self.cleaned_data["last_name"],
email=self.cleaned_data["email"],
)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
And the view (some stuff left out):
class RegistrationView(FormView):
template_name = 'register.html'
form_class = BaseCreationForm
model = UserProfile
success_url = '/account/login/'
def form_valid(self, form):
form = BaseCreationForm(self.request.POST,
self.request.FILES)
user = form.save()
profile = user.get_profile()
profile.user_type = form.cleaned_data['user_type']
profile.title = form.cleaned_data['title']
profile.company_name = form.cleaned_data['company_name']
.
.
.
profile.save()
return super(RegistrationView, self).form_valid(form)
You shouldn't be re-instantiating the form inside the form_valid method. That's called when the form is already valid, and indeed the form is passed into the method. You should use that instead.
(Note that the actual error is because you haven't called form.is_valid() at all, but as I say above you shouldn't, because the view is already doing it.)
I need set field value, not passed to Django Form constructor.
I have model and form like this:
class Message(models.Model):
created = models.DateTimeField()
text = models.CharField(max_length=200, blank=True, null=True)
active = models.BooleanField(default=False)
class MessageForm(forms.ModelForm):
class Meta:
model = Message
exclude = ('created', 'active')
def clean(self):
# check if user is blocked
if user.is_admin():
self.cleaned_data['active'] = True
return self.cleaned_data
Expected: if current user is admin - I need automatically set message as active. User should not pass this parameter by form.
Actual: I see that saved message always have flag "False" (I can delete condition and in this case I also see that message is not active).
Please help me understand, how can I do set this "active" flag in clean() method.
The previous answer would work, but I like encapsulating all the form's internal operations like what to show and what not, within the form. I know you mentioned you don't want to send a field value to the constructor, but if you don't mind sending the user, your solution would work.
i.e., your constructor:
def __init__(self, user):
self.user = user
super(BaseForm, self).__init__()
then in your clean, you just change the user to self.user.
There is another added benefit to this. Say tomorrow you want to assign more fields based on your user, you don't need to add anything to the views, you can simply add it to the form.
EDIT:
When you add a field to exclude, it is not available in the cleaned data. Instead, set its widget as hidden.
active = forms.BooleanField(widget=forms.HiddenInput)
EDIT 2: If you really don't want the field in the form
In this case, instead of overriding the clean, why don't you override the save?
def save (self):
super(BaseForm, self).save()
if user.is_admin():
self.instance.active=True
super(BaseForm, self).save()
Don't do this in the form's clean() method, do this in the view.
def your_view(request):
if request.method == 'POST':
form = MessageForm(data=request.POST)
if form.is_valid():
new_message = form.save(commit=False)
if user.is_admin():
new_message.active = True
However, if you also want to handle the case where your user is not admin using the same form, you can look at incorporating similar logic in the form's init() instead of the view, probably by passing info about the user from the view to the form's init()
Use this:
def message_form_factory(user):
class MessageForm(forms.ModelForm):
def clean(self):
# check if user is blocked
if user.is_admin():
self.cleaned_data['active'] = True
return self.cleaned_data
return MessageForm
And in your view use:
form = message_form_factory(request.user)()
form = message_form_factory(request.user)(request.POST)