How can I test an unchecked checkbox in Django? - django

So I present a checkbox to the user, as you can see inside my forms.py file:
class AddFooToBarForm(forms.Form):
to_original = forms.BooleanField(initial=True,
error_messages={'required':
"Oh no! How do I fix?"})
...
Depending on whether the user checks or unchecks that checkbox, I do something different, as you can see inside my views.py file:
def add_foo_to_bar(request, id):
...
try:
bar = Bar.objects.get(pk=id)
if request.method == 'POST': # handle submitted data
form = AddFooToBarForm(bar, request.POST)
if form.is_valid(): # good, now create Foo and add to bar
...
to_original = form.cleaned_data['to_original']
print 'to original is {0}'.format(to_original) # for debugging
if to_original:
# do A
else:
# do B
...
So I want to test that my site does indeed perform the correct actions, depending on whether the user checks the checkbox or not, inside my tests.py file:
class FooTest(TestCase):
...
def test_submit_add_to_Bar(self):
form_data = {
...
'to_original': True,
}
response = self.client.post(reverse('add_foo_to_bar', args=(self.bar.id,)),
form_data, follow=True)
self.assertContains(...) # works
...
form_data['to_original'] = None
response = self.client.post(reverse('add_foo_to_bar', args=(self.bar.id,)),
form_data, follow=True)
print response # for debugging purposes
self.assertContains(...) # doesn't work
I've tried
del form_data['to_original'] -- this gives me the "Oh no! How do I fix?" error message
form_data['to_original'] = None -- in my view function, I get True, so A is done instead of B
form_data['to_original'] = False -- this gives me the "Oh no! How do I fix?" error message once again
So how should I test the user not checking the checkbox in Django? (I'm using the latest version, 1.4.3)

When checkbox is not checked, its not present in submitted form. Also, when submitted value is 'on'.
If you want to make BooleanField optional have required=False while defining the form field.
Documentation BooleanField.

Related

How to check with pytest if one function was called inside Django form save method?

What i Have
Within the save method of the form are called two functions: generate_random_password and send_email, I need to know if these functions were called because generate_random_password assigns a password randomly to each new created user and send_email sends a email notification to the user with the credentials, the password generated and the user to login. It is important to know if these functions were executed correctly within save.
class UserAdminCreationForm(forms.ModelForm):
class Meta:
model = User
fields = ()
def save(self, commit: bool = True) -> U:
user = super(UserAdminCreationForm, self).save(commit=False)
data = self.cleaned_data
# Set random password for every new user
random_password = generate_random_password()
user.set_password(random_password)
# Send email confirmation with credentials to login
email = data.get("email")
html_message = render_to_string(
template_name="mails/user_creation_notification.html",
context={"email": email, "password": random_password},
)
# Strip the html tag. So people can see the pure text at least.
plain_message = strip_tags(html_message)
send_mail(
subject="Bienvenido a TodoTránsito",
message=plain_message,
recipient_list=[email],
html_message=html_message,
)
# Save into the DB the new user
if commit:
user.save()
return user
The problem
I'm using pytest to test my Django code, but I don't know how to assert if these functions were called.
From what you wrote in the comments, I would guess mocking both functions could make sense, as you probably don't really need the random password in your test. You could do something like:
from unittest import mock
#mock.patch('some_module.generate_random_password', return_value='swordfish')
#mock.patch('some_module.send_mail')
def test_save_user(mocked_send_mail, mocked_generate_password):
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
# or mocked_send_mail.assert_called_once_with(...) if you want to check the parameters it was called with
Note that you have to make sure to mock the correct module, e.g. the one used in the tested code (see where to patch).
In this case generate_random_password is replaced with a mock that always returns the same password, and send_mail is replaced by a mock that does nothing except recording calls. Both mocks can be accessed via the arguments in the test, that are injected by the patch decorators (last decorator first, the argument names are arbitrary).
If you install pytest-mock, you get the mocker fixture, that gives you the same functionality and more. The same code would like like this:
def test_save_user(mocker):
mocked_generate_password = mocker.patch('some_module.generate_random_password', return_value='swordfish')
mocked_send_mail = mocker.patch('some_module.send_mail')
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
If you now want to use the real generate_random_password, but still want to see if it was called, you can use mocker.spy instead:
def test_save_user(mocker):
mocked_generate_password = mocker.spy(some_module, 'generate_random_password')
mocked_send_mail = mocker.patch('some_module.send_mail')
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
Note that you can achieve the same with unittest.mock.patch.object, but less convenient, in my opinion.

Django signup/login not directing users to ?next=/my/url

Setup
I use [django-allauth][1] for user accounts.
# urls.py
url(r'^login$', allauth.account.views.login, name="account_login"),
url(r'^join$', allauth.account.views.signup, name="account_signup"),
.
# settings.py
LOGIN_REDIRECT_URL = '/me'
LOGIN_URL = '/join' # users sent here if they run into #login_required decorator
# To collect additional info if user signs up by email:
ACCOUNT_SIGNUP_FORM_CLASS = 'allauth.account.forms.WinnerSignupForm'
.
That custom signup form:
# account/forms.py
from .models import Winner, FriendCode
class WinnerSignupForm(forms.ModelForm):
"""
This is the additional custom form to accompany the default fields email/password (and maybe username)
"""
class Meta:
model = Winner
fields = ('author_display_name','signupcode',)
widgets = {'author_display_name':forms.TextInput(attrs={
'placeholder': _('Display Name'), # 'Display Name',
'autofocus': 'autofocus',
})
,
'signupcode': forms.TextInput(attrs={
'placeholder': _('Invite code (optional)'),
'autofocus': 'autofocus'
})
}
def signup(self, request, user):
# custom code that performs some account setup for the user
# just runs a procedure; there's no "return" at end of this block
I don't think my custom WinnerSignupForm is causing the issue, because the problem persists even if I disable it (i.e., I comment out this line from settings.py: ACCOUNT_SIGNUP_FORM_CLASS = 'allauth.account.forms.WinnerSignupForm')
Behaviour
0. Without ?next=/some/url parameter:
Thanks to LOGIN_REDIRECT_URL in settings.py, if I visit example.com/join or example.com/login, I'll wind up on example.com/me
That is fine.
1. If I am already logged in, everything works as expected:
A) If I visit https://example.com/login?next=/some/url,
I'm immediately forwarded to https://example.com/some/url (without being asked to log in, since I am already logged in).
I conclude the /login view is correctly reading the next=/some/url argument.
B) Similarly, if I visit https://example.com/join?next=/some/url, I'm immediately forwarded to https://example.com/some/url.
I conclude the /join view is also correctly reading the next=/some/url argument.
2. If I log in or sign up by social account, everything works as expected
This uses allauth/socialaccount
After I sign up or log in, I'm forwarded to https://example.com/some/url
However, here's the problem:
3. But! If I log in by email, ?next=/some/url is being ignored:
A) If I visit https://example.com/login?next=/some/url, I'm brought first to the /login page.
If I log in by email, I'm then forwarded to https://example.com/me
For some reason now, the ?next= is not over-riding the default LOGIN_REDIRECT_URL in settings.
(If I log in via Twitter, the ?next= paramter is correctly read, and I'm brought to https://example.com/some/url.)
B) Similarly, if I visit https://example.com/join?next=/some/url, I'm brought first to the /join (signup) page, and after successful login by email, I'm brought to /me, i.e., the fallback LOGIN_REDIRECT_URL defined in settings.py.
Inspecting the POST data in the signup/login form, the "next" parameter is there alright: {"next": "/some/url", "username": "myusername", "password": "..."}
More context
Extracts from django-allauth:
# allauth/account/views.py
from .utils import (get_next_redirect_url, complete_signup,
get_login_redirect_url, perform_login,
passthrough_next_redirect_url)
...
class SignupView(RedirectAuthenticatedUserMixin, CloseableSignupMixin,
AjaxCapableProcessFormViewMixin, FormView):
template_name = "account/signup.html"
form_class = SignupForm
redirect_field_name = "next"
success_url = None
def get_form_class(self):
return get_form_class(app_settings.FORMS, 'signup', self.form_class)
def get_success_url(self):
# Explicitly passed ?next= URL takes precedence
ret = (get_next_redirect_url(self.request,
self.redirect_field_name)
or self.success_url)
return ret
...
.
# allauth/account/utils.py
def get_next_redirect_url(request, redirect_field_name="next"):
"""
Returns the next URL to redirect to, if it was explicitly passed
via the request.
"""
redirect_to = request.GET.get(redirect_field_name)
if not is_safe_url(redirect_to):
redirect_to = None
return redirect_to
def get_login_redirect_url(request, url=None, redirect_field_name="next"):
redirect_url \
= (url
or get_next_redirect_url(request,
redirect_field_name=redirect_field_name)
or get_adapter().get_login_redirect_url(request))
return redirect_url
_user_display_callable = None
...
I'm pretty sure it was originally working when I installed [django-allauth][1] out of the box. I must have somehow interfered to break this ?next=/some/url functionality, though I can't remember the last time it was working or find out what I've done to mess things up.
Any tips on troubleshooting would be greatly appreciated.
(In case relevant -- perhaps settings are not being read correctly;
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True in settings.py seems to be ignored, users have to log in after resetting their password.)
#Akshay, the following work-around worked for me.
I added the following lines to allauth/account/adapter.py, within the get_login_redirect_url sub-function.
goto = request.POST.get('next', '')
if goto:
return goto
To clarify, the result looks like this:
class DefaultAccountAdapter(object):
# no change to stash_verified_email, unstash_verified_email, etc.
# edit only get_login_redirect_url as follows
def get_login_redirect_url(self, request):
"""
Returns the default URL to redirect to after logging in. Note
that URLs passed explicitly (e.g. by passing along a `next`
GET parameter) take precedence over the value returned here.
"""
assert request.user.is_authenticated()
url = getattr(settings, "LOGIN_REDIRECT_URLNAME", None)
if url:
warnings.warn("LOGIN_REDIRECT_URLNAME is deprecated, simply"
" use LOGIN_REDIRECT_URL with a URL name",
DeprecationWarning)
else:
url = settings.LOGIN_REDIRECT_URL
# Added 20170301 - look again for ?next parameter, as work-around fallback
goto = request.POST.get('next', '')
if goto:
return goto
print "found next url in adapter.py"
else:
print "no sign of next in adapter.py"
# end of work-around manually added bit
return resolve_url(url)
# leave remainder of fn untouched
# get_logout_redirect_url, get_email_confirmation_redirect_url, etc.
I still don't know how I broke this functionality in the first place, so I won't mark my answer as accepted/best answer. It does, however, resolve the issue I had, so I am happy. Hope this is useful to others.

How can I ignore a field validation in flask-wtf?

I have a form to add an item to my database, which includes two buttons: Cancel and Submit. The problem I have is that when I press the Cancel button with an empty form, I get a Please fill out this field. error instead of returning to my home page (see views.py for logic). So how can I get my app to ignore the DataRequired validators when I press the Cancel button?
forms.py:
class ItemForm(FlaskForm):
id = StringField('id', validators=[DataRequired()]
name = StringField('Name', validators=[DataRequired()]
cancel = SubmitField('Cancel')
submit = SubmitField('Submit')
views.py:
def add_item()
form = ItemForm()
if form.validate_on_submit():
if form.submit.data:
# Code to add item to db, removed for brevity.
elif form.cancel.data:
flash('Add operation cancelled')
return redirect(url_for('home.homepage'))
Your cancel button doesn't really need to be a submit button. You can simply have a normal button which takes the user back to the home page (using a href or capturing the onclick event).
If you still want the cancel button to be a WTForms field, one option would be to override the validate method in the form and remove the DataRequired validators on id and name. The below is untested but may give you a starting point to work from.
class ItemForm(FlaskForm):
id = StringField('id')
name = StringField('Name')
cancel = SubmitField('Cancel')
submit = SubmitField('Submit')
def validate(self):
rv = Form.validate(self)
if not rv:
return False
if self.cancel.data
return True
if self.id.data is None or self.name.data is None:
return False
return True

submitting form results into db - django

i created a form to save a post into db for my blog project. I've designed index page. now i am tryin to create a form to create new posts. before that i was using ' manage.py shell'
here is my view :
def addpost(request):
form = addForm()
if request.method=="POST":
titleform = request.POST['title']
bodyform = request.POST['body']
checkform = request.POST['isdraft']
if form.is_valid():
n = Post(title = titleform, body = bodyform, isdraft=checkform)
n.save()
return HttpResponseRedirect('/admin/')
else:
pass
return render(request,'userside/add.html',{'form':form,})
my model.py:
class Post(models.Model):
title = models.CharField(max_length = 100)
body = models.TextField()
slug = AutoSlugField(populate_from='title',unique=True)
posted = models.DateField(auto_now_add=True)
isdraft = models.BooleanField()
def __unicode__(self):
return self.title
#permalink
def get_absolute_url(self):
return ('view_blog_post',None, {'postslug':self.slug})
class addForm(forms.Form):
title = forms.CharField(max_length=100)
body = forms.CharField(widget=forms.Textarea)
isdraft = forms.BooleanField()
if i submit form as 'isdraft' field is False(unchecked) ; it gives error like:
MultiValueDictKeyError at /admin/addpost/
"Key 'isdraft' not found in "
and if i submit the form as 'isdraft' field is True(checked) ; it gives nothing. just refreshing form. no adding data into db.
i am doing sth wrong..
thank you
edit : Dmitry Beransky's answer worked for checkbox error. but it still doesnt add any data into db. just refreshes the form.
The whole point of using a form is that it takes care of validation and cleaning, that is converting values to the proper data types. That's why you should be accessing form.cleaned_data rather than reques.POST, and you should be doing it inside the if form.is_valid() check.
Edit
I've just noticed that you're never passing request.POST to the form. So form.is_valid() will never be true.
Please go back and read the documentation about using a form in a view.
If a checkbox is not checked in your HTML form, it's name/value is not going to be included in the data that the browser sends to your server. Which meanst that the request.POST dictionary is not going to contain an entry for 'isdraft' which in turn will cause a key error when you try to read the isdraft value. A solution is to change the way you read the value from the posted data to:
checkform = request.POST.get('isdraft', False)
rather than throw an error if isdraft isn't found in the dictionary, this will set checkform to False (the default value in case of a missing key)
Maybe your form does not validate at all. Have you checked if your code even reaches those lines after the if form.is_valid() statement ? If they do, what you've done there is right and should create the db row for your new entry, though you could have used
Post.objects.create(....) , and that would have taken away the need for calling the method save().
Some points though:
instead of checking for request.POST , check for request.method == 'POST' , cause there might be a post which has an empty POST dict ( in case no arguments have been submitted ), in that case request.POST fails to provide the right check .
see the docs for more info : request.POST
instead of using request.POST['var_name'] , use request.POST.get('var_name', 'default_value') , cause doing this like request.POST['var_name'] might result in some exceptions ( in case for example the argument is not provided , like what happened for your checkform variable )
Try accessing those variables through form.cleaned_data
and finally , you don't need the else statement in the end , just use the indentation :)

Django formset unit test

I can't run a unit test with formset.
I try to do a test:
class NewClientTestCase(TestCase):
def setUp(self):
self.c = Client()
def test_0_create_individual_with_same_adress(self):
post_data = {
'ctype': User.CONTACT_INDIVIDUAL,
'username': 'dupond.f',
'email': 'new#gmail.com',
'password': 'pwd',
'password2': 'pwd',
'civility': User.CIVILITY_MISTER,
'first_name': 'François',
'last_name': 'DUPOND',
'phone': '+33 1 34 12 52 30',
'gsm': '+33 6 34 12 52 30',
'fax': '+33 1 34 12 52 30',
'form-0-address1': '33 avenue Gambetta',
'form-0-address2': 'apt 50',
'form-0-zip_code': '75020',
'form-0-city': 'Paris',
'form-0-country': 'FRA',
'same_for_billing': True,
}
response = self.c.post(reverse('client:full_account'), post_data, follow=True)
self.assertRedirects(response, '%s?created=1' % reverse('client:dashboard'))
and I have this error:
ValidationError: [u'ManagementForm data is missing or has been
tampered with']
My view :
def full_account(request, url_redirect=''):
from forms import NewUserFullForm, AddressForm, BaseArticleFormSet
fields_required = []
fields_notrequired = []
AddressFormSet = formset_factory(AddressForm, extra=2, formset=BaseArticleFormSet)
if request.method == 'POST':
form = NewUserFullForm(request.POST)
objforms = AddressFormSet(request.POST)
if objforms.is_valid() and form.is_valid():
user = form.save()
address = objforms.forms[0].save()
if url_redirect=='':
url_redirect = '%s?created=1' % reverse('client:dashboard')
logon(request, form.instance)
return HttpResponseRedirect(url_redirect)
else:
form = NewUserFullForm()
objforms = AddressFormSet()
return direct_to_template(request, 'clients/full_account.html', {
'form':form,
'formset': objforms,
'tld_fr':False,
})
and my form file :
class BaseArticleFormSet(BaseFormSet):
def clean(self):
msg_err = _('Ce champ est obligatoire.')
non_errors = True
if 'same_for_billing' in self.data and self.data['same_for_billing'] == 'on':
same_for_billing = True
else:
same_for_billing = False
for i in [0, 1]:
form = self.forms[i]
for field in form.fields:
name_field = 'form-%d-%s' % (i, field )
value_field = self.data[name_field].strip()
if i == 0 and self.forms[0].fields[field].required and value_field =='':
form.errors[field] = msg_err
non_errors = False
elif i == 1 and not same_for_billing and self.forms[1].fields[field].required and value_field =='':
form.errors[field] = msg_err
non_errors = False
return non_errors
class AddressForm(forms.ModelForm):
class Meta:
model = Address
address1 = forms.CharField()
address2 = forms.CharField(required=False)
zip_code = forms.CharField()
city = forms.CharField()
country = forms.ChoiceField(choices=CountryField.COUNTRIES, initial='FRA')
In particular, I've found that the ManagmentForm validator is looking for the following items to be POSTed:
form_data = {
'form-TOTAL_FORMS': 1,
'form-INITIAL_FORMS': 0
}
Every Django formset comes with a management form that needs to be included in the post. The official docs explain it pretty well. To use it within your unit test, you either need to write it out yourself. (The link I provided shows an example), or call formset.management_form which outputs the data.
It is in fact easy to reproduce whatever is in the formset by inspecting the context of the response.
Consider the code below (with self.client being a regular test client):
url = "some_url"
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# data will receive all the forms field names
# key will be the field name (as "formx-fieldname"), value will be the string representation.
data = {}
# global information, some additional fields may go there
data['csrf_token'] = response.context['csrf_token']
# management form information, needed because of the formset
management_form = response.context['form'].management_form
for i in 'TOTAL_FORMS', 'INITIAL_FORMS', 'MIN_NUM_FORMS', 'MAX_NUM_FORMS':
data['%s-%s' % (management_form.prefix, i)] = management_form[i].value()
for i in range(response.context['form'].total_form_count()):
# get form index 'i'
current_form = response.context['form'].forms[i]
# retrieve all the fields
for field_name in current_form.fields:
value = current_form[field_name].value()
data['%s-%s' % (current_form.prefix, field_name)] = value if value is not None else ''
# flush out to stdout
print '#' * 30
for i in sorted(data.keys()):
print i, '\t:', data[i]
# post the request without any change
response = self.client.post(url, data)
Important note
If you modify data prior to calling the self.client.post, you are likely mutating the DB. As a consequence, subsequent call to self.client.get might not yield to the same data, in particular for the management form and the order of the forms in the formset (because they can be ordered differently, depending on the underlying queryset). This means that
if you modify data[form-3-somefield] and call self.client.get, this same field might appear in say data[form-8-somefield],
if you modify data prior to a self.client.post, you cannot call self.client.post again with the same data: you have to call a self.client.get and reconstruct data again.
Django formset unit test
You can add following test helper methods to your test class [Python 3 code]
def build_formset_form_data(self, form_number, **data):
form = {}
for key, value in data.items():
form_key = f"form-{form_number}-{key}"
form[form_key] = value
return form
def build_formset_data(self, forms, **common_data):
formset_dict = {
"form-TOTAL_FORMS": f"{len(forms)}",
"form-MAX_NUM_FORMS": "1000",
"form-INITIAL_FORMS": "1"
}
formset_dict.update(common_data)
for i, form_data in enumerate(forms):
form_dict = self.build_formset_form_data(form_number=i, **form_data)
formset_dict.update(form_dict)
return formset_dict
And use them in test
def test_django_formset_post(self):
forms = [{"key1": "value1", "key2": "value2"}, {"key100": "value100"}]
payload = self.build_formset_data(forms=forms, global_param=100)
print(payload)
# self.client.post(url=url, data=payload)
You will get correct payload which makes Django ManagementForm happy
{
"form-INITIAL_FORMS": "1",
"form-TOTAL_FORMS": "2",
"form-MAX_NUM_FORMS": "1000",
"global_param": 100,
"form-0-key1": "value1",
"form-0-key2": "value2",
"form-1-key100": "value100",
}
Profit
There are several very useful answers here, e.g. pymen's and Raffi's, that show how to construct properly formatted payload for a formset post using the test client.
However, all of them still require at least some hand-coding of prefixes, dealing with existing objects, etc., which is not ideal.
As an alternative, we could create the payload for a post() using the response obtained from a get() request:
def create_formset_post_data(response, new_form_data=None):
if new_form_data is None:
new_form_data = []
csrf_token = response.context['csrf_token']
formset = response.context['formset']
prefix_template = formset.empty_form.prefix # default is 'form-__prefix__'
# extract initial formset data
management_form_data = formset.management_form.initial
form_data_list = formset.initial # this is a list of dict objects
# add new form data and update management form data
form_data_list.extend(new_form_data)
management_form_data['TOTAL_FORMS'] = len(form_data_list)
# initialize the post data dict...
post_data = dict(csrf_token=csrf_token)
# add properly prefixed management form fields
for key, value in management_form_data.items():
prefix = prefix_template.replace('__prefix__', '')
post_data[prefix + key] = value
# add properly prefixed data form fields
for index, form_data in enumerate(form_data_list):
for key, value in form_data.items():
prefix = prefix_template.replace('__prefix__', f'{index}-')
post_data[prefix + key] = value
return post_data
The output (post_data) will also include form fields for any existing objects.
Here's how you might use this in a Django TestCase:
def test_post_formset_data(self):
url_path = '/my/post/url/'
user = User.objects.create()
self.client.force_login(user)
# first GET the form content
response = self.client.get(url_path)
self.assertEqual(HTTPStatus.OK, response.status_code)
# specify form data for test
test_data = [
dict(first_name='someone', email='someone#email.com', ...),
...
]
# convert test_data to properly formatted dict
post_data = create_formset_post_data(response, new_form_data=test_data)
# now POST the data
response = self.client.post(url_path, data=post_data, follow=True)
# some assertions here
...
Some notes:
Instead of using the 'TOTAL_FORMS' string literal, we could import TOTAL_FORM_COUNT from django.forms.formsets, but that does not seem to be public (at least in Django 2.2).
Also note that the formset adds a 'DELETE' field to each form if can_delete is True. To test deletion of existing items, you can do something like this in your test:
...
post_data = create_formset_post_data(response)
post_data['form-0-DELETE'] = True
# then POST, etc.
...
From the source, we can see that there is no need include MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT in our test data:
MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of the management form, but only for the convenience of client-side code. The POST value of them returned from the client is not checked.
This doesn't seem to be a formset at all. Formsets will always have some sort of prefix on every POSTed value, as well as the ManagementForm that Bartek mentions. It might have helped if you posted the code of the view you're trying to test, and the form/formset it uses.
My case may be an outlier, but some instances were actually missing a field set in the stock "contrib" admin form/template leading to the error
"ManagementForm data is missing or has been tampered with"
when saved.
The issue was with the unicode method (SomeModel: [Bad Unicode data]) which I found investigating the inlines that were missing.
The lesson learned is to not use the MS Character Map, I guess. My issue was with vulgar fractions (¼, ½, ¾), but I'd assume it could occur many different ways. For special characters, copying/pasting from the w3 utf-8 page fixed it.
postscript-utf-8