Passing a query into Django test - django

I want to test a view of my Django application.
def search(request):
query = request.GET.get('query')
query_lower = query.lower()
list_name = re.split("[- ’? ; , ' . : ' ' " " ]",query_lower)
stripped_query = [strip_accents(x) for x in list_name]
clean_query =[word for word in stripped_query if word not in stopwords]
match_list = []
for x in Product.objects.all():
match = 0
for y in clean_query:
if y in x.name or y in x.brand:
match += 1
if match == len(clean_query):
match_list.append(x.id)
else:
pass
if not query:
products_list= Product.objects.all()
else:
products_list = Product.objects.filter(id__in=match_list)
context = {
'products': products_list,
}
return render(request, 'finder/search.html', context)
I did create some products in my tests.py with setup and I want to test if I have a 200 status code if on of those products is searched:
def test_Search(self):
self.response = self.client.post(reverse(('finder:search'), {'query':'gazpacho'}))
self.assertEqual(self.response.status_code, 200)
I got a TypeError: unhashable type: 'dict'.
So, how I am supposed to pass my query into my test for it to be run?

Your view handles GET-parameters so the request itself is a GET-request.
In your test you are sending a post, which should be a get accordingly:
self.client.get(url, {'query': '...'})
Parameters are passed as the second argument for get()/post.
See more in the docs.
In your case most likely (without having the full trace of the error) your error is the way you are calling reverse() in your test.
The second argument passed to that function is urlconf. From the docs:
The urlconf argument is the URLconf module containing the URL patterns to use for reversing. By default, the root URLconf for the current thread is used.

Something like this?
class TestKlass(...):
def test_Search(self):
url = reverse('finder:search')
url_with_param = "{}?{}".format(url, 'query=gazpacho')
self.response = self.client.post(url_with_param)
self.assertEqual(self.response.status_code, 200)

Related

Django redirect and modify GET parameters

I am implementing magic tokens and would like clean URLs. As a consequence, I would like to remove the token from the URL upon a successful user authentication. This is my attempt:
def authenticate_via_token(get_response):
def middleware(request):
if request.session.get('authenticated', None):
pass
else:
token = request.GET.get('token', None)
if token:
mt = MagicToken.fetch_by_token(token)
if mt:
request.session['authenticated'] = mt.email
if not request.GET._mutable:
request.GET._mutable = True
request.GET['token'] = None
request.GET._mutable = False
else:
print("invalid token")
response = get_response(request)
return response
return middleware
IE, I would like to send /products/product-detail/3?token=piyMlVMrmYblRwHcgwPEee --> /products/product-detail/3
It's possible that there may be additional GET parameters and I would like to keep them. Any input would be appreciated!
This is the solution I ended up going for:
from django.urls import resolve, reverse
import urllib
def drop_get_param(request, param):
'helpful for redirecting while dropping a specific parameter'
resolution = resolve(request.path_info) #simulate resolving the request
new_params = request.GET.copy() # copy the parameters
del new_params[param] # drop the specified parameter
reversed = reverse(resolution.url_name, kwargs=resolution.kwargs) # create a base url
if new_params: #append the remaining parameters
reversed += '?' + urllib.parse.urlencode(new_params)
return reversed

Django Testing view template context

I'm trying to test the
return render(request, 'template.html', context)
and seem to be falling short. Is it not worth while testing this? Or if it is worth while testing this, how do I accomplish that?
view.py
def create_employee_profile(request):
name_form = EmployeeNameForm()
context = {'name_form':name_form}
return render(request,
'template_create_employee_profile.html',
context
)
I know the if: else: statements are missing. I didn't think they were relevant to the test.
test.py
# TEST: context({'name_form':name_form})
def test_CreateEmployeeProfileView_context(self):
name_form = EmployeeNameForm()
response = self.client.get(reverse(
'create_employee_profile'))
self.assertEquals(response.context['name_form'], name_form)
This got me the closest to success. Here's my error:
AssertionError: <Empl[27 chars]alid=False,
fields=(employee_choices;first_nam[20 chars]ame)> !=
<Empl[27 chars]alid=Unknown,
fields=(employee_choices;first_n[22 chars]ame)>
What about the detail view?
# TEST: context({'name':name})
def test_CustomerEmployeeProfileView_context(self):
name = CustomerEmployeeName.objects.get(pk=1)
response = self.client.get(
reverse('service:customer_employee_profile_detail', kwargs={'pk': 1}))
self.assertIsInstance(response.context['name'], name)
Got this error:
TypeError: isinstance() arg 2 must be a type or tuple of types
You are comparing two different instances of the EmployeeNameForm, which is why the assertion fails.
If you just want to test that the context variable is indeed a EmployeeNameForm then you can test it with assertIsInstance:
def test_CreateEmployeeProfileView_context(self):
response = self.client.get(reverse('create_employee_profile'))
self.assertIsInstance(response.context['name_form'], EmployeeNameForm)

Django url r'^page/ should redirect to the same dynamic template whatever argument behind

I have a little problem with my django website's urls.
In fact I have:
urlpatterns = i18n_patterns('',
url(r'^test/(?P<variable_name>\w+)/$', views.testUrl),
I have a dynamic page created with the name "test". redirected from the view after some treatment :
def testUrl(request, variable_name):
current_site = Site.objects.get_current()
urlTest = HttpRequest.get_full_path(request)
parseIt = variable_name.split("/")
...
for x in parseIt:
if x == 'm':
if ToFind[:1] == 'm':
ID = ToFind[1:]
else:
ID = ToFind
try:
context_ret = GetId(x, api_url, ID, api_key, context_ret)
except Exception as err:
context_ret['message'] = err
return render(request, 'base_templates/test.html', context_ret)
elif x == 'z':
try:
context_ret = GetId(x, api_url, ToFind, api_key, context_ret)
except Exception as err:
context_ret['message'] = err
return render(request, 'base_templates/test.html', context_ret)
return render(request, 'base_templates/test.html', context_ret)
So if I type mydomain.org/test/ I have my dynamic page showing. Perfect.
But if I do mydomain.org/test/{whatever} I have the test.html template rendered but not the dynamic page !
Thus, the problem is that I have dynamic plugins within this dynamic page, I need to - whatever is behind test/ - use the same dynamic page. And not juste the template.
Without changing the url..
Is there a way of doing it ?
Edit:
here is an example of a call:
domain.org/test/1923/
Since you want to wirk with {whatever} comming from /test/{whatever}, you need to declare a variable in your url or pass {whatever} via GET variable.
Declaring a variable in url:
Change your url definition like this
...
url(r'^test/(?P<variable_name>\w+)/$', views.testUrl),
...
And catch it in the view:
def testUrl(request, variable_name):
# Now if you call '/test/hithere/'
# variable_name will take the value 'hithere'
# you could do -> parseIt = variable_name
...
Passing {whatever} via GET variable
You can always call you url like this:
mydomain.org/test/?var=hithere
And the in your view:
def testUrl(request):
parsetIt = request.GET.get('var')
...

Using named group to call function in view django

I don't know if something like this is possbile but I would like to call a function based on the named group value in the urls.py config.
This is what I have been looking at
url.py snippet
url(r'^ajax/ValidateLogin/(?P<funcname>\w{13})/$', Validate),
views.py snippet
def checkUsername(request):
user = User.objects.get(username=request.POST.get('username',''))
if user == None:
response = simplejson.dumps({'success':'True','validate':'True'})
else:
response = simplejson.dumps({'success':'True','validate':'False'})
return HttpResponse(response,content_type='application/javascript; charset=utf-8')
def Validate(request,funcname):
return funcname(request)
This just returns that unicode object isn't callable which I understand but how do I then convert it to call the view function check username? An example of url that should call this is ajax/ValidateLogin/checkUsername
Thanks
remote_function_calls = {
'check_username' : check_username,
}
def Validate(request, funcname):
try:
return remote_function_calls[funcname](request)
except KeyError:
response = http.HttpResponse("Invalid function")
response.status_code = 403
return response
Another method to define which functions are valid
def valid_function(request):
return http.HttpResponse("This function may be called remotely")
valid_function.remote_call_valid = True
def erase_everything_function(request):
world.delete()
valid_functions = dict([(k, v) for k, v in globals().iteritems() if
getattr(v, 'remote_call_valid', None) == True])
Use this:
def Validate(request,funcname):
return locals()[funcname](request)
Although this kind of thing is a bit of a security risk since any user can call any arbitrary function available to python by passing the right "funcname".

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