Django testing, mock valid requests to urls - python

I have a bunch of urls in my urls.py file that have the login_required decorator
# Index Page
url(r'^$', login_required(views.IndexPage.as_view()), name='index'),
# Schedule urls
url(r'^schedules/$', login_required(views.ScheduleListView.as_view()),
name='schedule-list'),
url(r'^schedule/(?P<pk>[\d]+)/$',
login_required(views.ScheduleDetailView.as_view()),
name='schedule-detail'),
url(r'^schedule-freeze/(?P<pk>[\d]+)/$',
login_required(views.freezeSchedule),
name='schedule-freeze'),
url(r'^schedule-create/$', login_required(views.ScheduleCreate.as_view()),
name='schedule-create'),
url(r'^schedule-delete/(?P<pk>[\d]+)$',
login_required(views.ScheduleDelete.as_view()),
name='schedule-delete'),
url(r'^schedule-update/(?P<pk>[\d]+)/$',
login_required(views.ScheduleUpdate.as_view()),
name='schedule-update'),
url(r'^schedule-generate/(?P<pk>[\d]+)/$',
login_required(views.scheduleGenerate), name='schedule-generate'),
# Client urls
url(r'^clients/$', login_required(views.ClientList.as_view()),
name='client-list'),
url(r'^client/(?P<slug>[\w-]+)/$',
login_required(views.ClientDetail.as_view()), name='client-detail'),
url(r'^client-create/$', login_required(views.ClientCreate.as_view()),
name='client-create'),
url(r'^client-delete/(?P<slug>[\w-]+)/$',
login_required(views.ClientDelete.as_view()), name='client-delete'),
url(r'^client-update/(?P<slug>[\w-]+)/$',
login_required(views.ClientUpdate.as_view()), name='client-update'),
# And so on ....
For every restricted view I'm trying to write a test which ensures unauthorized users are redirected to the login page when trying to access the view. If possible I'd like to be able to achieve this in a single block of code, instead of writing a single test for every single URL.
I've tried something like the following:
list_urls = [e for e in get_resolver(urls).reverse_dict.keys() if isinstance(e, str)]
for url in list_urls:
# Fetches the urlpath e.g. 'client-list'
namedspaced_url = 'reports:' + url
path = reverse(namedspaced_url)
response = self.client.get(path)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, reverse('login') + '?next=' + path)
list_urls returns a list of all the named urls inside my urls.py file i.e. ['schedule-create', 'server-detail', 'schedule-list', 'schedule-update', 'index', ....]
The Problem
this piece of code: reverse(namedspaced_url)
Where this causes issues is that each url has a different regular expression pattern, i.e. some take slugs some take pk's
so the line path = reverse(namedspaced_url) will work for simple URLs like those which point at ListViews but will fail for more complex URLs, such as those that point at DetailViews which require slug's/pk's, i.e. path = reverse(namedspaces_url, args=[1945])
Is it possible to temporarily override / ignore Django's pattern matching / routing to force a request to go through (regardless of passed args)
Or do I have to manually write a test for each URL with valid kwargs/args to satisfy regex?
Is there another completely different approach I can take to write tests for all my login_required() views?
Update
Using introspection I came up with the following monstrosity to solve my problem
def test_page_redirects_for_unauthorised_users(self):
url_dict = get_resolver(urls).reverse_dict
url_list = [e for e in get_resolver(urls).reverse_dict.keys() if
isinstance(e, str)]
for url in url_list:
patterns = url_dict[url][0][0][1]
matches = [1 if e == 'pk' else "slug" if e == 'slug' else None for
e in patterns]
path = reverse('reports:' + url, args=matches)
response = self.client.get(path)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, reverse('login') + '?next=' + path)

You're trying to test something very complicated because you've decided to use login_required to decorate the urlconf.
Why not decorate the class instead? That way you can simply test each class to make sure it has the login_required decorator. This eliminates the need for mocking slug and pk regex values.

in the proect_name/project_name/urls.py
urlpatterns = [
url(r'', login_required(include('app_name.urls')),
]
This will apply login_required to all urls in the project_name/app_name/urls.py

Related

How can I reuse a DRF class-based views POST method?

Now I want to reuse the post method of BundleList. Either I find out the absolute URL and use requests.post(URL) to send a request.
The 2nd way is to reuse by return BundleList.as_view()(request) in a view function. But I can't set request.data = data. Request data is immutable.
When I try to use
url = reverse_lazy(BundleList.as_view(), request=request)
print(f"{url = }")
It just gives me:
NoReverseMatch at /generateSampleBundle/
Reverse for 'my_app.views.BundleList' not found. 'my_app.views.BundleList' is not a valid view function or pattern name.
The BundleList is a class-based view with get and post method.
drfurlpatterns = [ # DRF URL endpoints
path('bundles/', views.BundleList.as_view()),
]
Can anyone help me out?
You should set name for your view and use that name in reverse_lazy()
for ex:
drfurlpatterns = [ # DRF URL endpoints
path('bundles/', views.BundleList.as_view()), name='bundle-list'
]
then
url = reverse_lazy('bundle-list', request=request)
see, docs
Also, to know why request.data is sometimes immutable see this question

return to function from another function

i have a table which include all users and two columns at the end (Edit,Delete) and i just enabled the delete column, the issue is when i click on the delete icon the record will be deleted but the url will stuck on the delete function even if i used return render(request,'getUsersInfo.html') which is get all records function
Model Name: Users
urls:
from django.urls import path
from django.conf.urls import url
from . import views
urlpatterns = [
path('signup.html',views.signup,name=''),
path('getUsersInfo.html',views.getAllUsers,name=''),
url(r'^deleteUser/(?P<fullname>\D+)/$',views.deleteUser, name='deleteUser'),
# this is how to call a function without parameters url(r'^deleteUser/$',views.deleteUser, name='deleteUser'),
in the same view i have 3 functions (singup "add user", getAllUsers "get all the records to the table,deleteUser)
views:
def getAllUsers(request):
print("getAllUsers")
thesearchValue = ''
if 'SearchValue' in request.GET:
thesearchValue = request.GET['SearchValue']
print(request.GET['SearchValue'])
allUsers = User.objects.filter(fullname__icontains=thesearchValue)#all()
# return render(request,'getUsersInfo.html',{'allUsers':allUsers})
return render(request,'getUsersInfo.html',{'allUsers':allUsers})
else:
print("Empty")
allUsers = User.objects.all()
return render(request,'getUsersInfo.html',{'allUsers':allUsers})
def deleteUser(request,fullname):
print('delete the user')
todelete = User.objects.filter(fullname=fullname)
todelete.delete()
return render(request,'getUsersInfo.html')
Notice that i used return render(request,'getUsersInfo.html') which should call getAllUsers(request): but the url stuck on http://127.0.0.1:8000/deleteUser/John/
Rendering the same template as another view does not mean that you will somehow call other views. A template is nothing more than a tool to specify how to convert context data to a string, that is passed as HTTP response. You can use the same template in multiple views, and a view can render multiple templates.
You can make use of redirect(..) [Django-doc] to return a HTTP redirect response (302):
from django.shortcuts import redirect
def deleteUser(request,fullname):
print('delete the user')
todelete = User.objects.filter(fullname=fullname)
todelete.delete()
return redirect(getAllUsers)
Note: A GET request is not supposed to have side-effects, hence removing
objects when a user makes a GET request, is not compliant with the HTTP
standard. Therefore it might be better to remove a User with a POST request.

Expand url django patterns

i have standard django 1.4 url patterns:
urlpatterns = patterns('',
url('^',include('events.urls')),
url(r'^$', home, {'template_name':'index.html','mod':None}, name='home'),
url(r'^contact$',contact, {'template_name':'index.html',
'mod':'contacto'},name='contact'),
url('^task/(?P<task_id>[\w+-]+)',celery_tasks,name='tasks'),
)
I want to build my sitemap.xml leaving out some urls, for example that /task url should not appear(it makes no sense for the web spiders).
My strategy is passing all the url patterns to my Sitemap class, like this
from sitemaps import EventsSitemap, StaticSitemap
sitemaps = {
'Events': CandidateSiteMap,
'static': StaticSitemap(urlpatterns),
}
As you can see i´m passing the patterns to the class, so i can later filter the urls like this
class StaticSitemap(Sitemap):
def __init__(self, patterns):
self.patterns = patterns
self._items = {}
self._initialize()
def _initialize(self):
do_not_show = ['tasks']
for p in self.patterns:
# no dynamic urls in this class (we handle those separately)
if not p.regex.groups:
if getattr(p,'name',False) and p.name not in do_not_show:
self._items[p.name] = self._get_modification_date(p)
So i keep this list of do_not_show url names and thats how i filter out urls, so far so good, the problem is with included urls such as:
url('^',include('events.urls')),
I can't just iterate on self.patterns and get the included urls, i have to expand them first, thats my question, how can i do that?
How can i get a flat list of urls as if there were no included ones, all were on a single urls module.
Any recommendations to filter out urls in the sitemaps.xml would be most appreciated.
Ok i have to answer my own question because i solved it, what i did was a little function to expand the patterns like this
def expand_patterns(patterns):
new_patterns = []
def recursive_expand(patterns):
for p in patterns:
if getattr(p,'url_patterns',False):
recursive_expand(p.url_patterns)
else:
new_patterns.append(p)
recursive_expand(patterns)
return new_patterns
This will flatten out the urlpatterns into a single list.
So now i can use self.patterns to filter out anything in my Sitemap class :)

How to use one app to satisfy multiple URLs in Django

I'm trying to use one app to satisfy multiple url paths. That is to say, I want the url /blog/ and /job/ to use the same app, but different views. There are a number of ways to do this I'm sure, but none of them seem very clean. Here's what I'm doing right now
# /urls.py
urlpatterns = patterns("",
(r"^(blog|job)/", include("myproject.myapp.urls")),
)
# /myapp/urls.py
urlpatterns = patterns("myproject.myapp.views",
(r"^(?P<id>\d+)/edit/$", "myproject.myapp.views.edit"),
(r"^(?P<id>\d+)/delete/$", "myproject.myapp.views.delete"),
(r"^(?P<id>\d+)/update/$", "myproject.myapp.views.update"),
(r"^insert/$", "myproject.myapp.views.insert"),
)
urlpatterns += patterns("",
(r"^(?P<object_id>\d+)/$", "django.views.generic.list_detail.object_detail", info_dict, "NOIDEA-detail"),
(r"^/$", "django.views.generic.list_detail.object_list", info_dict, "NOIDEA-community"),
)
# /myapp/views.py
def edit(request, type, id):
if (type == "blog"):
editBlog(request, id)
else (type == "job")
editJob(request, id)
def editBlog(request, id):
# some code
def editJob(request, id):
# some code
I've ended up breaking all of this into multiple model and view files to make the code cleaner but the above example doesn't account for things like reverse url lookups which breaks all of my template {% url %} calls.
Originally, I had blogs, jobs, events, contests, etc all living in their own apps, but all of their functionality is so similar, that it didn't make sense to leave it that way, so I attempted to combine them... and this happened. You see those "NOIDEA-detail" and "NOIDEA-community" url names on my generic views? Yeah, I don't know what to use there :-(
You can have more than one modules defining URLs. You can have /blog/ URLs in myapp/urls.py and /job/ URLs in myapp/job_urls.py. Or you can have two modules within a urls subpackage.
Alternatively you can manually prefix your url definitions:
urlpatterns = patterns("myproject.myapp.views",
(r"^jobs/(?P<id>\d+)/edit/$", "myproject.myapp.views.edit"),
(r"^jobs/(?P<id>\d+)/delete/$", "myproject.myapp.views.delete"),
(r"^jobs/(?P<id>\d+)/update/$", "myproject.myapp.views.update"),
(r"^jobs/insert/$", "myproject.myapp.views.insert"),
)
urlpatterns += patterns("",
(r"^blog/(?P<object_id>\d+)/$", "django.views.generic.list_detail.object_detail", info_dict, "NOIDEA-detail"),
(r"^blog/$", "django.views.generic.list_detail.object_list", info_dict, "NOIDEA-community"),
)
And then mount them as:
urlpatterns = patterns("",
(r"", include("myapp.urls")),
)
Personally I would go for more RESTful URL definitions though. Such as blog/(?P<post_id>\d+)/edit/$.
Looks pretty good to me. If you want reverse lookups, just have a different reverse name for each url format, even if they end up pointing to the same view.

django urls without a trailing slash do not redirect

I've got two applications located on two separate computers. On computer A, in the urls.py file I have a line like the following:
(r'^cast/$', 'mySite.simulate.views.cast')
And that url will work for both mySite.com/cast/ and mySite.com/cast. But on computer B I have a similar url written out like:
(r'^login/$', 'mySite.myUser.views.login')
For some reason on computer B the url mySite.com/login/ will work but mySite.com/login will hang and won't direct back to mySite.com/login/ like it will on computer A. Is there something I missed? Both url.py files look identical to me.
Or you can write your urls like this:
(r'^login/?$', 'mySite.myUser.views.login')
The question sign after the trailing slash makes it optional in regexp. Use it if for some reasons you don't want to use APPEND_SLASH setting.
check your APPEND_SLASH setting in the settings.py file
more info in the django docs
This improves on #Michael Gendin's answer. His answer serves the identical page with two separate URLs. It would be better to have login automatically redirect to login/, and then serve the latter as the main page:
from django.conf.urls import patterns
from django.views.generic import RedirectView
urlpatterns = patterns('',
# Redirect login to login/
(r'^login$', RedirectView.as_view(url = '/login/')),
# Handle the page with the slash.
(r'^login/', "views.my_handler"),
)
I've had the same problem too. My solution was put an (|/) before the end line of my regular expression.
url(r'^artists/(?P[\d]+)(|/)$', ArtistDetailView.as_view()),
Append slash without redirect, use it instead of CommonMiddleware in settings, Django 2.1:
MIDDLEWARE = [
...
# 'django.middleware.common.CommonMiddleware',
'htx.middleware.CommonMiddlewareAppendSlashWithoutRedirect',
...
]
Add to your main app directory middleware.py:
from django.http import HttpResponsePermanentRedirect, HttpRequest
from django.core.handlers.base import BaseHandler
from django.middleware.common import CommonMiddleware
from django.conf import settings
class HttpSmartRedirectResponse(HttpResponsePermanentRedirect):
pass
class CommonMiddlewareAppendSlashWithoutRedirect(CommonMiddleware):
""" This class converts HttpSmartRedirectResponse to the common response
of Django view, without redirect.
"""
response_redirect_class = HttpSmartRedirectResponse
def __init__(self, *args, **kwargs):
# create django request resolver
self.handler = BaseHandler()
# prevent recursive includes
old = settings.MIDDLEWARE
name = self.__module__ + '.' + self.__class__.__name__
settings.MIDDLEWARE = [i for i in settings.MIDDLEWARE if i != name]
self.handler.load_middleware()
settings.MIDDLEWARE = old
super(CommonMiddlewareAppendSlashWithoutRedirect, self).__init__(*args, **kwargs)
def process_response(self, request, response):
response = super(CommonMiddlewareAppendSlashWithoutRedirect, self).process_response(request, response)
if isinstance(response, HttpSmartRedirectResponse):
if not request.path.endswith('/'):
request.path = request.path + '/'
# we don't need query string in path_info because it's in request.GET already
request.path_info = request.path
response = self.handler.get_response(request)
return response
In some cases, we have issues when some of our users call API with different endings. Usually, our users use Postman for that and are not worried about slash at the endpoint. As result, we receive issue requests in support, because users forgot to append a slash / at the end of POST requests.
We solved it by using a custom middleware that works for us in Django 3.2+ and Django 4.0+. After that, Django may handle any POST/PUT/DELETE requests to your API with slash or without them. With this middleware unneeded to change APPEND_SLASH property in settings.py
So, in the settings.py need to remove your current 'django.middleware.common.CommonMiddleware' and insert new middleware. Make sure, you change your_project_name in my example below on your real project name.
MIDDLEWARE = [
...
# 'django.middleware.common.CommonMiddleware',
'your_project_name.middleware.CommonMiddlewareAppendSlashWithoutRedirect',
...
]
Add to your main app directory middleware.py:
from django.http import HttpResponsePermanentRedirect, HttpRequest
from django.core.handlers.base import BaseHandler
from django.middleware.common import CommonMiddleware
from django.utils.http import escape_leading_slashes
from django.conf import settings
class HttpSmartRedirectResponse(HttpResponsePermanentRedirect):
pass
class CommonMiddlewareAppendSlashWithoutRedirect(CommonMiddleware):
""" This class converts HttpSmartRedirectResponse to the common response
of Django view, without redirect. This is necessary to match status_codes
for urls like /url?q=1 and /url/?q=1. If you don't use it, you will have 302
code always on pages without slash.
"""
response_redirect_class = HttpSmartRedirectResponse
def __init__(self, *args, **kwargs):
# create django request resolver
self.handler = BaseHandler()
# prevent recursive includes
old = settings.MIDDLEWARE
name = self.__module__ + '.' + self.__class__.__name__
settings.MIDDLEWARE = [i for i in settings.MIDDLEWARE if i != name]
self.handler.load_middleware()
settings.MIDDLEWARE = old
super(CommonMiddlewareAppendSlashWithoutRedirect, self).__init__(*args, **kwargs)
def get_full_path_with_slash(self, request):
""" Return the full path of the request with a trailing slash appended
without Exception in Debug mode
"""
new_path = request.get_full_path(force_append_slash=True)
# Prevent construction of scheme relative urls.
new_path = escape_leading_slashes(new_path)
return new_path
def process_response(self, request, response):
response = super(CommonMiddlewareAppendSlashWithoutRedirect, self).process_response(request, response)
if isinstance(response, HttpSmartRedirectResponse):
if not request.path.endswith('/'):
request.path = request.path + '/'
# we don't need query string in path_info because it's in request.GET already
request.path_info = request.path
response = self.handler.get_response(request)
return response
This answer may look similar to Max Tkachenko answer. But his code didn't work for me in the latest versions of Django.
I've had the same problem. In my case it was a stale leftover from some old version in urls.py, from before staticfiles:
url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL.lstrip('/'),
'django.views.static.serve',
kwargs={'document_root': settings.MEDIA_ROOT}),
MEDIA_URL was empty, so this pattern matched everything.

Categories