14: Ajax Development With JSON Renderers

Modern web apps are more than rendered HTML. Dynamic pages now use JavaScript to update the UI in the browser by requesting server data as JSON. Pyramid supports this with a JSON renderer.

Background

As we saw in 08: HTML Generation With Templating, view declarations can specify a renderer. Output from the view is then run through the renderer, which generates and returns the Response. We first used a Chameleon renderer, then a Jinja2 renderer.

Renderers aren't limited, however, to templates that generate HTML. Pyramid supplies a JSON renderer which takes Python data, serializes it to JSON, and performs some other functions such as setting the content type. In fact, you can write your own renderer (or extend a built-in renderer) containing custom logic for your unique application.

Steps

  1. First we copy the results of the view_classes step:

    $ cd ..; cp -r view_classes json; cd json
    $ $VENV/bin/python setup.py develop
    
  2. We add a new route for hello_json in json/tutorial/__init__.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    from pyramid.config import Configurator
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        config.include('pyramid_chameleon')
        config.add_route('home', '/')
        config.add_route('hello', '/howdy')
        config.add_route('hello_json', 'howdy.json')
        config.scan('.views')
        return config.make_wsgi_app()
    
  3. Rather than implement a new view, we will "stack" another decorator on the hello view in views.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from pyramid.view import (
        view_config,
        view_defaults
        )
    
    
    @view_defaults(renderer='home.pt')
    class TutorialViews:
        def __init__(self, request):
            self.request = request
    
        @view_config(route_name='home')
        def home(self):
            return {'name': 'Home View'}
    
        @view_config(route_name='hello')
        @view_config(route_name='hello_json', renderer='json')
        def hello(self):
            return {'name': 'Hello View'}
    
  4. We need a new functional test at the end of json/tutorial/tests.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    import unittest
    
    from pyramid import testing
    
    
    class TutorialViewTests(unittest.TestCase):
        def setUp(self):
            self.config = testing.setUp()
    
        def tearDown(self):
            testing.tearDown()
    
        def test_home(self):
            from .views import TutorialViews
    
            request = testing.DummyRequest()
            inst = TutorialViews(request)
            response = inst.home()
            self.assertEqual('Home View', response['name'])
    
        def test_hello(self):
            from .views import TutorialViews
    
            request = testing.DummyRequest()
            inst = TutorialViews(request)
            response = inst.hello()
            self.assertEqual('Hello View', response['name'])
    
    
    class TutorialFunctionalTests(unittest.TestCase):
        def setUp(self):
            from tutorial import main
            app = main({})
            from webtest import TestApp
    
            self.testapp = TestApp(app)
    
        def test_home(self):
            res = self.testapp.get('/', status=200)
            self.assertIn(b'<h1>Hi Home View', res.body)
    
        def test_hello(self):
            res = self.testapp.get('/howdy', status=200)
            self.assertIn(b'<h1>Hi Hello View', res.body)
    
        def test_hello_json(self):
            res = self.testapp.get('/howdy.json', status=200)
            self.assertIn(b'{"name": "Hello View"}', res.body)
            self.assertEqual(res.content_type, 'application/json')
    
    
  5. Run the tests:

    $ $VENV/bin/nosetests tutorial
    
  6. Run your Pyramid application with:

    $ $VENV/bin/pserve development.ini --reload
    
  7. Open http://localhost:6543/howdy.json in your browser and you will see the resulting JSON response.

Analysis

Earlier we changed our view functions and methods to return Python data. This change to a data-oriented view layer made test writing easier, decoupling the templating from the view logic.

Since Pyramid has a JSON renderer as well as the templating renderers, it is an easy step to return JSON. In this case we kept the exact same view and arranged to return a JSON encoding of the view data. We did this by:

  • Adding a route to map /howdy.json to a route name
  • Providing a @view_config that associated that route name with an existing view
  • overriding the view defaults in the view config that mentions the hello_json route, so that when the route is matched, we use the JSON renderer rather than the home.pt template renderer that would otherwise be used.

In fact, for pure Ajax-style web applications, we could re-use the existing route by using Pyramid's view predicates to match on the Accepts: header sent by modern Ajax implementation.

Pyramid's JSON renderer uses the base Python JSON encoder, thus inheriting its strengths and weaknesses. For example, Python can't natively JSON encode DateTime objects. There are a number of solutions for this in Pyramid, including extending the JSON renderer with a custom renderer.