== Synopsis ==
Often (usually in middleware) processing has to be applied to certain URLs only eg CORS.
The usual way to specify this would be to create an additional set of regex patterns identifying these urls - eg.
CORS_URLS_REGEX = r'^/api/2/.*$'
JSONP_URLS = r'^/api/1/.*$'
PRIVATE_URLS = r'/(private|api)/.*$'
Each middleware then typically matches the incoming request URL to the regex and determines whether it is to be selected for processing by it.
This approach has several limitations including:
* It violates DRY as the regexes in the settings have to be synced with the actual URL patterns
* Matching multiple patterns either requires the user to create complex regexes or the app/middleware writer has to essentially reinvent URL patterns - poorly.
== The Proposal ==
Add an optional tags keyword argument to django.conf.urls.url allowing a URL to be optionally tagged with one or more tags which can then be retrieved via HttpRequest.resolver_match.tags in the middleware / view (or any code with access to urlpatterns - not necessarily in the context of a request). Probably easiest to explain via examples:
urlpatterns = [
url(r'^$', views.home, name='home'),
url(r'^private/$', include(private_patterns), tags=['private']),
url(r'^api/1/', include(api_v1_patterns), tags=[
'api', 'private', 'jsonp',
]),
url(r'^api/2/', include(api_v1_patterns), tags=[
'api', 'cors', 'private',
]),
]
api_v1_patterns = [
url(r'^list/books/$', views.list_books, name='list-books'),
url(r'^list/articles/$', views.list_articles, name='list-articles', tags=['public]),
...
]
api_v2_patterns = [
url(r'^list/books/$', views.list_books, name='v2-list-books'),
url(r'^list/articles/$', views.list_articles, name='v2-list-articles',),
...
]
In the above patterns all URLs under /private/ are tagged 'private', all URLs under /api/1/ are tagged 'api', 'jsonp' and 'private'.
Some examples to show how you can access and use tags
Example Middleware:
class PrivatePagesMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
"""
For any url tagged with 'private', check if the user is authenticated. The presence of a
'public' tag overrides the 'private' tag and no check should be performed.
Authentication depends on whether the URL is marked as 'cors' or not. 'cors' urls
use HTTP header token authentication
"""
tags = request.resolver_match.tags
if 'private' in tags and not 'public' in tags:
if 'cors' in tags:
# CORS requests are authenticated via tokens in the headers
# check auth tokens
...
if not authenticated:
return HttpResponseForbidden()
elif not request.user.is_authenticated(): # normal django auth
return redirect('login')
class CorsMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
if 'cors' in request.resolver_match.tags:
# continue CORS processing
def process_response(self, request, response):
if 'cors' in request.resolver_match.tags:
# continue CORS processing
Example Management command:
commands/exportapi.py
"""
Javascript API code generator
Iterate through urlpatterns, for each url tagged with 'api' export a Javascript function
that allows js code to call the api function. Depending on whether the pattern is tagged
'jsonp' or 'cors' write the corresponding type of function
"""
def get_api_urls(urlpatterns, api_type):
for pattern in urlpatterns:
# check if pattern has the 'api' tag and the api_type tag
....
if is_api_type:
yield pattern
class Command(BaseCommand):
def handle():
for api_pattern in get_api_urls(urlpattrns, 'jsonp'):
# write JSONP javascript function to stdout
for api_pattern in get_api_urls(urlpattrns, 'cors'):
# write CORS javascript function to stdout
manage.py exportapi > api.js
---------------------------------------------------------------------
The actual code change required to enable the tags feature is about 10 lines. All that the urls code does is to make the tags (after combining included patterns) available to the match object (which is already available to the request object).
The corresponding proposal there is to add a decorators tag to django.conf.urls.url allowing
url(r'^private/'), include(private_patterns), decorators=[login_required]),
This will apply the decorator login_required to all the urls under /private/
If what you wanted to do was to apply the decorator to all views then this is undoubtedly very convenient and does the job perfectly.
However decorators are not the most convenient mechanism for:
1. Whitelisting as opposed to Blacklisting where a group of URLs is by default private except for the ones marked public. Writing a login_required decorator is straightforward, however writing a login_exempt decorator will always involve using the decorator to 'tag' the view and then check the tag in the middleware (eg. the csrf_exempt decorator). Using a decorator to 'mark' a view is heavyweight and needs to be done carefully (using functools etc) to ensure that it works correctly in the presence of other decorators.
2. Selecting a URL on the basis of a combination of decorators is not straightforward. Applying multiple decorators effective ANDs them however ORing or other logic is convoluted if actually possible. With string tags this is trivial.
3. Decorators are most useful in the context of a request as they are applied when the URL is actually resolved. On the other hand checking if a URL is tagged does not necessarily involve resolving the url allowing them to be more easily used in management commands etc
In addition to the above:
* Tagging is more 'semantic' - tagging a URL as 'private' does not enforce the use of the login_required decorator - there could be a completely different mechanism used which could change over time.
* Tagging a URL has no side effects other than they being copied over to the match object. The urls mechanism does not have to care about if/how the tags are actually used.
* More lightweight when all you want to do is 'mark' the URL.
The linked pull request is fully functional and includes tests but not documentation - which I can add at short notice.
All comments welcome!
Atul