Skip to content

Instantly share code, notes, and snippets.

@Naddiseo
Created March 17, 2015 21:46
Show Gist options
  • Save Naddiseo/39f2154b906a709ac26c to your computer and use it in GitHub Desktop.
Save Naddiseo/39f2154b906a709ac26c to your computer and use it in GitHub Desktop.
Django url serialization for use in javascript. Requires `simple-fmt`, `lodash`, and ES6. (Note, will not work as is)
import last from 'lodash/array/last';
import isObject from 'lodash/lang/isObject';
export default function pyargs(...args) {
let kwargs = last(args);
if (!isObject(kwargs)) {
kwargs = {};
}
else {
args.pop();
}
return [args, kwargs];
}
import re
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
from django.utils.regex_helper import normalize
from project.views.base import AjaxView
py2js_rx = re.compile('\(\?P\<\w+>(?P<innerrx>[^\)]+)\)')
def make_js_rx(s):
return py2js_rx.sub('(\g<innerrx>)', s)
class GetURLsView(AjaxView):
def _make_resolver(self, p = None, children = None, name = '', namespace_so_far = '') -> dict:
rx = make_js_rx(p.regex.pattern) if p is not None else '^/$'
ret = {
'type' : 'Resolver',
'name' : name,
'regex' : rx,
'normalized' : normalize(rx),
'namespace' : (p.namespace or '') if p is not None else '',
'children' : []
}
namespace_so_far = ':'.join(filter(None, [namespace_so_far, ret['namespace']]))
c = self._traverse_resolver(p, namespace_so_far) if children is None else children
if c:
ret['children'] = c
return ret
def _traverse_resolver(self, p: RegexURLResolver, namespace_so_far : str) -> dict:
assert isinstance(p, RegexURLResolver)
assert hasattr(p, 'url_patterns')
return self._traverse(p.url_patterns, p.namespace, namespace_so_far)
def _traverse(self, patterns, namespace = '', namespace_so_far = ''):
ret = []
for p in patterns:
if isinstance(p, RegexURLResolver):
resolver = self._make_resolver(p, None, '', namespace_so_far)
if not p.namespace:
# Promote a RegexResolver
normalized = normalize(p.regex.pattern)
for child in resolver['children']:
child['normalized'] = normalized + child['normalized']
child['regex'] = resolver['regex'] + child['regex'][1:]
#assert name not in ret
ret.append(child)
else:
ret.append(resolver)
elif isinstance(p, RegexURLPattern):
view_name = ':'.join(filter(None, [namespace_so_far, p.name]))
if not self.security.can_see(view_name):
continue
ret.append({
'type' : 'Pattern',
'name' : p.name if p.name else namespace,
'normalized' : normalize(p.regex.pattern),
'regex' : py2js_rx.sub(r'(\g<innerrx>)', p.regex.pattern)
})
return ret
def get(self, *args, **kwargs):
from project import urls
self.context = self._make_resolver(None, self._traverse(urls.urlpatterns))
import assert from 'assert'; // TODO: remove
import has from 'lodash/object/has';
import pyargs from './pyargs';
class NoReverseMatch extends Error {
constructor(path, args, kwargs, msg) {
var m = "Could not find url `{0}` with args `{1}` and kwargs `{2}`".format(
path, JSON.stringify(args), JSON.stringify(kwargs)
);
if (msg) {
m += ": {0}".format(msg);
}
this.message = m;
super(m);
}
}
function reversePatterns(argNames, args, kwargs, path) {
var ret = '';
argNames.forEach(function([patString, names]) {
var replacements = {};
var lastIdx = -1;
if (args.length && names.length) {
assert.ok(args.length >= names.length, "Not enough args for all named parameters");
names.forEach(function(name) {
var value = args.shift();
var replacement = '%({0})s'.format(name);
lastIdx = patString.indexOf(replacement, lastIdx);
if (lastIdx < 0) {
throw new NoReverseMatch(path, args, kwargs, "Could not find pattern for arg {0}".format(name));
}
ret += "{0}{1}".format(patString.slice(0, lastIdx), value);
lastIdx += replacement.length;
});
}
else if (Object.keys(kwargs).length && names.length) {
assert.ok(Object.keys(kwargs).length >= names.length, "Not enough kwargs for all named parameters");
names.forEach(function(name) {
if (!has(kwargs, name)) {
throw new NoReverseMatch(path, args, kwargs, "expected kwarg `{0}`".format(name));
}
var value = kwargs[name];
var replacement = '%({0})s'.format(name);
lastIdx = patString.indexOf(replacement, lastIdx);
if (lastIdx < 0) {
throw new NoReverseMatch(path, args, kwargs, "Could not find pattern for arg {0}".format(name));
}
ret += "{0}{1}".format(patString.slice(0, lastIdx), value);
lastIdx += replacement.length;
});
}
else if (names.length) {
throw new NoReverseMatch(path, args, kwargs, "No args or kwargs specified but had {0} parameter(s) to fill".format(names.length));
}
else {
lastIdx = 0;
}
ret += patString.slice(lastIdx);
});
return ret;
}
class RegexURLPattern {
constructor(pattern, name, argNames) {
this.pattern = new RegExp(pattern);
this.name = name;
this.argNames = argNames;
}
/*
returns a constructed url given the args/kwargs
*/
reverse(path, ...params) {
var ret = '';
var [args, kwargs] = pyargs(...params);
assert.equal(path, this.name);
if (args.length && Object.keys(kwargs).length) {
throw new Error("Don't mix args and kwargs in call to reverse()");
}
return reversePatterns(this.argNames, args, kwargs, path);
}
static FromJson(s) {
assert.equal(s.type, 'Pattern');
return new RegexURLPattern(s.regex, s.name, s.normalized);
}
}
class RegexURLResolver {
constructor(pattern, name, argNames = [], ns = '') {
this.pattern = new RegExp(pattern);
this.name = name;
this.argNames = argNames;
this.ns = ns;
this.children = [];
this._nameToChild = {};
this._nsToChild = {};
}
/*
Returns the url as a string with the parameters substituted.
*/
reverse(path, ...params) {
var namespaces = path.split(':');
var viewName = namespaces.pop();
var [args, kwargs] = pyargs(...params);
var reverser, ret = reversePatterns(this.argNames, args, kwargs, path);
params = args.concat([kwargs]);
if (namespaces.length) {
if (!this._nsToChild[namespaces[0]]) {
throw new NoReverseMatch(path, args, kwargs);
}
reverser = this._nsToChild[namespaces[0]];
path = namespaces.slice(1).concat([viewName]).join(':');
}
else if (viewName) {
if (!this._nameToChild[viewName]) {
throw new NoReverseMatch(path, args, kwargs);
}
reverser = this._nameToChild[viewName];
}
else {
throw new NoReverseMatch(path, args, kwargs);
}
return '{0}{1}'.format(ret, reverser.reverse(path, ...params));
}
addChild(child) {
this.children.push(child);
if (child instanceof RegexURLResolver) {
this._nsToChild[child.ns] = child;
}
else if (child instanceof RegexURLPattern && child.name.length) {
this._nameToChild[child.name] = child;
}
}
static FromJson(s) {
assert.equal(s.type, 'Resolver');
var ret = new RegexURLResolver(s.regex, s.name, s.normalized, s.namespace);
s.children.forEach(function(child) {
switch (child.type) {
case 'Resolver':
ret.addChild(RegexURLResolver.FromJson(child));
break;
case 'Pattern':
ret.addChild(RegexURLPattern.FromJson(child));
break;
default:
throw new Error("Unknown url type: " + child.type);
}
});
return ret;
}
}
export { RegexURLResolver };
@Naddiseo
Copy link
Author

Python side:

from django.conf.urls import patterns, url,include
from url_serializer import GetURLsView
from . import views

ajax_views = patterns('',
    url(r'(?P<kwarg1>\d+)/view1$', views.endpoint1.as_view(), name='view1'),
   url(r'view2$', views.endpoint2.as_view(), name='view2'),
)
urlpatterns = patterns('',
    url(r'url-serializer$', GetURLsView.as_view()),
    url(r'ajax/$',   include(ajax_views, namespace='ajax')),
)

Usage on js side (with mithril):

import m from 'mithril';
import { RegexURLResolver } from 'urlresolver';
var urlConf = m.prop();
m.request({url: '/url-serializer', method: 'GET'}).then(RegexURLResolver.FromJson).then(urlConf);

// Later:
var url = urlConf().reverse('ajax:view1', {kwarg1: 1});
m.request({url: url, method: 'GET'})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment