Django get url regex by name - python

I have a case where I have defined some Django url patterns and now I want to retrieve the regular expression associated with a given pattern. I want that because I want to pass these regular expressions to the client so I can check urls in client as well ( I'm talking about browser side history manipulation ) and fire appropriate handlers ( in JavaScript ) when there is a match.
For example if I have:
# urls.py
urlpatterns = patterns("",
url(r"^$", Index.as_view(), name="index"),
url(r"^user/", include("User.urls", namespace="User")),
)
# User/urls.py
urlpatterns = patterns("",
url(r"^profile/(?P<slug>.*)$", GetProfile.as_view(), name="get_profile")
)
then I need the following function:
>>> get_regex("User:get_profile")
'^user/profile/(?P<slug>.*)$'
( or however Django translates it ). Note that I'm using namespaces. Any ideas? Django1.5.
Also I've managed to write a function that returns the urlpattern object associated with a passed name, however doing url.regex.pattern returns '^profile/(?P<slug>.*)$. So as you can see there is no leading ^user/.

There are several javascript reverse implementations out there.
http://djangojs.readthedocs.org/en/latest/djangojs.html#reverse-urls
https://github.com/version2/django-js-reverse
It's not the regex, but you could test the urls in your client code just like you do in the server, so it's even better in my opinion.
EDIT: Since you need to ignore URL arguments, you could get an idea from the source of django-js here. It already removes optional URL arguments, so it's probably very similar to what you describe.
The code iterates over every pattern removing the ?P from each argument subregex so you could just replace them with .*.
The point is you have in that source every regex you could possibly need to do your implementation. See the global patterns in lines 24-29.

So I've tried few things and finally I came up with my own solution. First I convert urlpatterns into a form which JavaScript understands:
import re
converter = re.compile(r"\?P<.*?>")
def recursive_parse(urlpatterns, lst):
for pattern in urlpatterns:
obj = {
"pattern": converter.sub("", pattern.regex.pattern)
}
if hasattr(pattern, "name") and pattern.name is not None:
obj["name"] = pattern.name
if hasattr(pattern, "namespace"):
obj["namespace"] = pattern.namespace
if hasattr(pattern, "url_patterns"):
if "urls" not in obj:
obj["urls"] = []
recursive_parse(pattern.url_patterns, obj["urls"])
lst.append(obj)
def generate_paths(urlpatterns):
paths = []
recursive_parse(urlpatterns, paths)
return paths
Then I call generate_paths(urlpatterns), JSON-stringify the result and pass it to JavaScript (note that in JavaScript I have to convert regular expressions as strings to RegExp objects). In JavaScript I have
var recursive_check = function(url, patterns, names, args) {
var l = patterns.length;
for (var i = 0; i < l; i++) {
var pat = patterns[i],
match = pat.pattern.exec(url);
pat.lastIndex = 0;
if (match) {
names.push(pat.namespace || pat.name);
var f = match.shift(),
url = url.replace(f, ""),
ml = match.length;
for (var j = 0; j < ml; j++) {
args.push(match[j]);
}
if (pat.urls) {
recursive_check(url, pat.urls, names, args);
}
break;
}
}
};
var fire_handler = function(url) {
var names = [], args = [];
recursive_check(url, patterns, names, args);
// do something...
};
Now in // do something... I can do something with names and args. For example I can keep a dictionary of named handlers, I can search for a handler (based on names) and call it with args.
That's the solution that works for me. Converting urlpatterns to JavaScript patterns might not be perfect (since converter seems to be a bit too simplified) but it works in most simple cases.

Try this:
from django.core.urlresolvers import get_resolver
resolver = get_resolver(None)
url = resolver.reversed_dict.getlist('get_profile')
if url:
pattern = url[0][1]

Not an answer but might be useful to someone else looking at this.
The following generates a list of all, complete url patterns in the Django project, including for nested URLRegexResolvers, based on #Freakish's code.
import re
from django.core.urlresolvers import get_resolver
converter = re.compile(r"\?P<.*?>")
def trim_leading_caret(s):
return s[1:] if s.startswith('^') else s
def recursive_parse(urlpatterns, lst, prefix=None):
for pattern in urlpatterns:
path = (prefix or '') + trim_leading_caret(converter.sub("", pattern.regex.pattern))
if hasattr(pattern, "url_patterns"):
recursive_parse(pattern.url_patterns, lst, path)
else:
lst.append('^' + path)
def generate_paths(urlpatterns):
paths = []
recursive_parse(urlpatterns, paths)
return paths
generate_paths(get_resolver(None))

As far as I understood, you want to be able to return the regex expression (and not the url) of a given view.
This is my sketch of solution:
The function url returns an instance of RegexURLResolver. This class does store the regex, because it calls LocaleRegexProvider on __init__ (in this line and this line).
So, I think that you can
reverse search the view plus namespace
get the tuple of that view from the tuple of tuples urlpatterns
return _regex of the first argument, LocaleRegexProvider._regex (or regex()), of the tuple respective to the view.
I'm not sure this works (didn't tested), neither that it is the best solution, but at least you have some links on where Django stores the regex.

Related

How to replace parameterized string?

My use-case is to return the redirection uri for the given uri.
URI's will be as follows:
/books
/books/economic-genious
/books/flight-mechanics
My regular expression to match the above URI's as follows:
/books(/(.*))?$
My destination is configured as follows: /ebooks$1. So that the above URI's will be converted to:
/ebooks
/ebooks/economic-genious
/ebooks/flight-mechanics
For this my existing Javascript code is:
function getMappedURI(uri) {
var exp = new RegExp('/books(/(.*))?$');
var destUri = '/ebooks$1';
var redirectUri = uri.replace(exp, destUri);
return redirectUri;
}
Unable to achieve the same in Python.
That's a difficult way to replace the beginning of strings. If it's all about the result I would do it this way
import re
uri_list = [
'/books',
'/books/economic-genious',
'/books/flight-mechanics',
]
def getMappedURI(uri):
return re.sub(r'^\/books', '/ebooks', uri)
for uri in uri_list:
print(getMappedURI(uri))
Result
/ebooks
/ebooks/economic-genious
/ebooks/flight-mechanics
If you need to use the original regular exprression this should work
import re
uri_list = [
'/books/',
'/books/economic-genious',
'/books/flight-mechanics',
]
def getMappedURI(uri):
return re.sub(r'\/books(\/(.*))?$', r'/ebooks\1', uri)
for uri in uri_list:
print(getMappedURI(uri))
Result
/ebooks/
/ebooks/economic-genious
/ebooks/flight-mechanics
Note that backslashes have been added before the slashes in your regular expression.
If you want to avoid that you must use
re.sub(r'/books(/(.*))?$'.replace('/', r'\/'), r'/ebooks\1', uri)

Restful API with web.py

In PHPs Slim I can do this:
$app->get('/table/{table}', function (Request $request, Response $response, $args) {
$table = $args['table'];
$mapper = new TableMapper($this, $table);
$res = $mapper->getTable();
return $response->withJson($res);
}
$app->get('/table/{table}/{id}', function (Request $request, Response $response, $args) {
$table = $args['table'];
$id = (int)$args['id'];
$mapper = new TableMapper($this, $table);
$res = $mapper->getTableById($id);
return $response->withJson($res);
}
Now I'm trying with web.py. I can do this:
urls = (
'/table/(.+)', 'table',
)
class table:
def GET( self, table ):
rows = db.select( table )
web.header('Content-Type', 'application/json')
return json.dumps( [dict(row) for row in rows], default=decimal_default )
but if I try to extend this by doing, e.g.:
urls = (
'/table/(.+)', 'table',
'/table/(.+)/(\d+)', 'table_by_id'
)
Processing never arrive at the second of the urls. Instead the code does a db.select on a table name which is "table/id", which of course errors.
How can I develop this to parse a url with id added?
web.py matches in order listed in urls, so switching the order is one way to solve your issue:
urls = (
'/table/(.+)/(\d+)', 'table_by_id',
'/table/(.+)', 'table'
)
Another piece of advice: Tighten up your regex, so you match more closely exactly what you're looking for. You'll find bugs sooner.
For example, you'll note your /table/(.+) will indeed match "/table/foo/1", because the regex .+ also matches /, so you might consider a pattern like ([^/]+) to match "everything" except a slash.
Finally, no need for a leading '^' or trailing '$' in your URLs, web.py always looks to match the full pattern. (Internally, it adds '^' and '$').
Try this one:
urls = (
'^/table/(.+)/$', 'table',
'^/table/(.+)/(\d+)/$', 'table_by_id'
)

Does django's URL Conf support mapping an argument to strings?

The Problem
I'm unsure of the best way to phrase this, but here goes: (note some of this may not be syntactically/semantically correct, as it's not my actual code, but I needed it to help explain what I'm asking)
Say I have a model the model Album:
Class Album(models.Model):
ALBUM_TYPE_SINGLE = 1
ALBUM_TYPE_DEMO = 2
ALBUM_TYPE_GREATEST_HITS = 3
ALBUM_CHOICES = (
(ALBUM_TYPE_SINGLE, 'Single Record'),
(ALBUM_TYPE_DEMO, 'Demo Album'),
(ALBUM_TYPE_GREATEST_HITS, 'Greatest Hits'),
)
album_type = models.IntegerField(choices=ALBUM_CHOICES)
And I want to have separate URLs for the various types of albums. Currently, the URL Conf is something like so:
urlpatterns = [
url(r'^singles/(?P<pk>.+)/$', views.AlbumView, name="singles"),
url(r'^demos/(?P<pk>.+)/$', views.AlbumView, name="demos"),
url(r'^greatest-hits/(?P<pk>.+)/$', views.AlbumView, name="greatest_hits"),
]
And when I want to serve the appropriate URL, I need to check the album_type manually:
if object.album_type == Album.ALBUM_TYPE_SINGLE:
return reverse('singles', object.id)
elif object.album_type == Album.ALBUM_TYPE_DEMO:
return reverse('demos', object.id)
elif object.album_type == Album.ALBUM_TYPE_GREATEST_HITS:
return reverse('greatest_hits', object.id)
However, this is cumbersome to do, and I'm wondering if there is a way to pass in the album_type field to the call to reverse and have it automatically get the URL based on that. i.e. something like this:
urlpatterns = [
url(r'^(?P<type>[singles|demos|greatest-hits])/(?P<pk>.+)/$', views.AlbumView, name="albums"),
]
and called with
reverse("albums", object.album_type, object.id)
Attempted solutions
I considered setting the choice strings to be
ALBUM_CHOICES = (
(ALBUM_TYPE_SINGLE, 'singles'),
(ALBUM_TYPE_DEMO, 'demos'),
(ALBUM_TYPE_GREATEST_HITS, 'greatest-hits'),
)
which would then allow me to send object.get_album_type_display() as a string variable for type, which works, however, I need to be able to use reverse to build the URL while only having access to the integer value of album_type and not the display value.
I know this is an oddly specific question for an oddly specific scenario, but if anyone has any kind of potential solutions, I'd be very grateful! Thank you in advance!
I would change the field to a CharField, and use the URL slug as the actual value rather than the display value:
Class Album(models.Model):
ALBUM_TYPE_SINGLE = 'singles'
ALBUM_TYPE_DEMO = 'demos'
ALBUM_TYPE_GREATEST_HITS = 'greatest-hits'
ALBUM_CHOICES = (
(ALBUM_TYPE_SINGLE, 'Single Record'),
(ALBUM_TYPE_DEMO, 'Demo Album'),
(ALBUM_TYPE_GREATEST_HITS, 'Greatest Hits'),
)
album_type = models.CharField(choices=ALBUM_CHOICES, max_length=50)
In your urls.py:
urlpatterns = [
url(r'^(?P<type>singles|demos|greatest-hits)/(?P<pk>.+)/$', views.AlbumView, name="albums"),
]
Then you can reverse it by passing album.album_type:
reverse('albums', args=(album.album_type, album.pk))

lambda as argument to jinja2 filter?

I'd like to have a custom filter in jinja2 like this:
{{ my_list|my_real_map_filter(lambda i: i.something.else)|some_other_filter }}
But when I implement it, I get this error:
TemplateSyntaxError: expected token ',', got 'i'
It appears jinja2's syntax does not allow for lambdas as arguments? Is there some nice workaround? For now, I'm creating the lambda in python then passing it to the template as a variable, but I'd rather be able to just create it in the template.
No, you cannot pass general Python expression to filter in Jinja2 template
The confusion comes from jinja2 templates being similar to Python syntax in many aspects, but you shall take it as code with completely independent syntax.
Jinja2 has strict rules, what can be expected at which part of the template and it generally does not allow python code as is, it expect exact types of expressions, which are quite limited.
This is in line with the concept, that presentation and model shall be separated, so template shall not allow too much logic. Anyway, comparing to many other templating options, Jinja2 is quite permissible and allows quite a lot of logic in templates.
I have a workaround, I'm sorting a dict object:
registers = dict(
CMD = dict(
address = 0x00020,
name = 'command register'),
SR = dict(
address = 0x00010,
name = 'status register'),
)
I wanted to loop over the register dict, but sort by address. So I needed a way to sort by the 'address' field. To do this, I created a custom filter and pass the lambda expression as a string, then I use Python's builtin eval() to create the real lambda:
def my_dictsort(value, by='key', reverse = False):
if by == 'key':
sort_by = lambda x: x[0].lower() # assumes key is a str
elif by == 'value':
sort_by = lambda x: x[1]
else:
sort_by = eval(by) # assumes lambda string, you should error check
return sorted(value, key = sort_by, reverse = reverse)
With this function, you can inject it into the jinja2 environment like so:
env = jinja2.Environment(...)
env.filters['my_dictsort'] = my_dictsort
env.globals['lookup'] = lookup # queries a database, returns dict
And then call it from your template:
{% for key, value in lookup('registers') | my_dict_sort("lambda x:x[1]['address']") %}
{{"""\
static const unsigned int ADDR_{key} = 0x0{address:04X}; // {name}
""" | format(key = key, address = value['address'], name = value['name'])
}}
{% endfor %}
Output:
static const unsigned int ADDR_SR = 0x00010; // status register
static const unsigned int ADDR_CMD = 0x00020; // command register
So you can pass a lambda as a string, but you'll have to add a custom filter to do it.
i've had to handle the same issue recently, I've had to create a list of dict in my Ansible template, and this is not included in the base filters.
Here's my workaroud :
def generate_list_from_list(list, target, var_name="item"):
"""
:param list: the input data
:param target: the applied transformation on each item of the list
:param var_name: the name of the parameter in the lambda, to be able to change it if needed
:return: A list containing for each item the target format
"""
# I didn't put the error handling to keep it short
# Here I evaluate the lambda template, inserting the name of the parameter and the output format
f = eval("lambda {}: {}".format(var_name, target))
return [f(item) for item in list]
# ---- Ansible filters ----
class FilterModule(object):
def filters(self):
return {
'generate_list_from_list': generate_list_from_list
}
I'm then able to use it this way :
(my_input_list is a listof string, but it would work with a list of anything)
# I have to put quotes between the <target> parameter because it evaluates as a string
# The variable is not available at the time of the templating so it would fail otherwise
my_variable: "{{ my_input_list | generate_list_from_list(\"{ 'host': item, 'port': 8000 }\") }}"
Or if I want to rename the lambda parameter :
my_variable: "{{ my_input_list | generate_list_from_list(\"{ 'host': variable, 'port': 8000 }\", var_name="variable") }}"
This outputs :
when called directly in Python:
[{'host': 'item1', 'port': 8000}, {'host': 'item2', 'port': 8000}]
when called in a template (a yaml file in my example, but as it returns a list you can do whatever you want once the list is transformed) :
my_variable:
- host: item1
port: 8000
- host: item2
port: 8000
Hope this helps someone

Django optional view parameter with HttpResponseRedirect

So I have a view that grabs a person's info from a query and returns the info to the page:
def film_chart_view(request, if_random = False):
I also have a view that randomly grabs a person's info and redirects it to the above view:
def random_person(request):
.
.
return HttpResponseRedirect(reverse('home.views.film_chart_view')+"?q="+get_person.short)
However, I want the first view to recognize if it came from the second view, so that if it is, it sets the if_random parameter to True, but I'm not exactly sure how to do that.
my urls:
url(r'^film_chart_view/$', 'home.views.film_chart_view'),
url(r'^random/$', 'home.views.random_person'),
You don't have to pass if_random as a url parameter.
def random_person(request):
return HttpResponseRedirect(
reverse('home.views.film_chart_view') + \
"?q=" + get_person.short + \
"&is_random=1"
)
def film_chart_view(request):
is_random = 'is_random 'in request.GET
But if you prefer url parameters, the solution is a little more complex.
The parameters passed to the view function comes from the url patterns, you need to set them at first.
Because the is_random para is optional, I suggest you to write 2 separated patterns for the film_chart_view.( actually you can combine these 2 patterns to one with a more complex regex expr, but readability counts.)
urlconf:
url(r'^film_chart_view/$', 'home.views.film_chart_view', name ='film_chart_view'),
url(r'^film_chart_view/(?P<is_random>.*)/$', 'home.views.film_chart_view', name ='film_chart_view_random'),
url(r'^random/$', 'home.views.random_person'),
def random_person(request):
return HttpResponseRedirect(
reverse('home.views.film_chart_view', kwargs={'is_random': '1'}) + \
"?q=" + get_person.short
)
The view parameters(except the request) are always strings, you need to convert it to int/bool/... in you code.
def film_chart_view(request, is_random=None):
if is_random:
...

Categories