Sunday, July 13, 2014

DjangoRESTFramework, AngularJS and working with URLs

I like to put constants, URL's, etc into a django view that returns JSON for my angular apps that use django as a backend. You can write an angular service that calls this view, fetches and initialises this data for other angular services to consume by creating one service for constants, one for URL-mappings, etc. Or you can use a jsonify template tag like the one here and directly embed it in a script tag into your index template. I prefer the latter - I don't think it's worth the extra request just to keep your angular templates and your django templates separate.
<script type="text/javascript">
    var CONF = {
        urls: {{ urls | jsonify }},
        constants: {{ constants | jsonify }}
    };
</script>

With DjangoRESTFramework
Suppose you have endpoints, like the ones below, registered using a router in DRF,
api_router = DefaultRouter()
api_router.register("products", ProductViewSet)
api_router.register("product_notes", ProductNotesViewSet)
you should be able to enumerate all URLs for a given api router and build mappings for them using the following snippet.
def get_urls_by_router(api_router, urlconf=None):
    urls = {}
    list_name = api_router.routes[0].name
    for prefix, _viewset, basename in api_router.registry:
        urls[prefix] = reverse(list_name.format(basename=basename), urlconf=urlconf)
    return urls
print get_urls_by_router(api_router)
# {"products": "/api/v1/product/products", "product_notes": "/api/v1/product/product_notes"}
And in angular, I use a service, like the one below, to refer to these URLs by name instead of hardcoding them all over the place.
module.factory("URL", function(conf){
    var urls = conf.urls;
    return function(name){
        if(urls.hasOwnProperty(name))
            return urls[name];
        else
            throw new Error("Unknown URL identifier: " + name);
    };
});
where conf.urls is a mapping of URL names to URL patterns.

Special note about Multi-tenant systems
Suppose you are using the Sites framework to run multiple sites on a single django instance, you are probably using separate urlconfs for each site (so that Site1's URLs aren't visible from Site2). In cases like this, the URL you are trying to reverse might not be part of the request urlconf (available in request.urlconf). The urlconf argument to get_urls_by_router() is for cases like this where you have to override the request urlconf.

Cross-Domain requests
Sometimes, you might choose to serve your REST endpoints and the rest of your app from different domains (for eg., your actual app is at app.com while API is at api.app.com). For this, I used Django CORS headers to CORS enable my views. I use something like this to refer to API URLs in my code if the API is hosted on a different domain.
module.factory("APIURL", function(URL, constants){
    var api_base_url = constants.api_base_url;
    return function(name){
        return api_base_url + URL(name);
    };
});
where constants.api_base_url can be something like 'http://api.site2.com/v1'.

Cross-Domain requests not setting cookies/getting blocked?
When serving your API from a secure https:// URL and using a self-signed certificate, you may notice weird behaviour in certain browsers. In Chrome, everything was OK, except for the fact that cookies set by my django views, although visible in the response, were being ignored. As for Firefox, POST requests were being blocked by the browser. Using a proper certificate fixed these issues. Just an FYI.

Navigation elements breaking in IE?
Lack of support for the HTML5 history API broke navigation on IE. Use something like the snippet below to render href attributes for <a></a> elements in your angular templates. I also attach the hashURL service to $rootScope so that it's available in all my templates.
module.run(function(..., hashURL , ...){
    $rootScope.hashURL = hashURL;
}).factory("hashURL", function(URL, $location){
    // Used to build hashbanged urls if the HTML5 history.pushState
    // API is unavailable.
    var hashURL;
    if(!$location.$$html5){
        hashURL = URL;
    } else {
        hashURL = function(name){
            return "#" + URL(name);
        };
    }
    return hashURL;
});
and in your templates, use it like this
View more <a href="{{ hashURL('products') }}">products</a>.
Now whether you decide to support IE or not, I still think it's a good idea to refer to URLs by name using a service in your templates.

Serving from localhost:8000 breaking ngResource?
In Angular, the : symbol is used to define replaceable components that are part of URLs in ngResource/djResource for eg., http://example.com/api/products/:id. You need to escape the ":" in URLs passed to these services. I use a URLParse service (borrowed from here) to parse the port number out of the URL to escape the ":" before it. Note, URLs passed to $http don't need this escaping.
module.factory("URLParse", function($window){
    var parser = document.createElement('a');
    return function(url){
        parser.href = url;
        return {
            href: parser.href,
            protocol: parser.protocol,
            host: parser.host,
            hostname: parser.hostname,
            port: parser.port,
            pathname: parser.pathname,
            hash: parser.hash,
            search: parser.search,
        };
    };
}).factory("ResourceURL", function(URL, URLParse){
    return function(name){
        var url = URL(name),
            port = URLParse(url).port;
        return url.replace("\:" + port, "\\:" + port);
    };
});

No comments:

Post a Comment