4: Type-Specific Views

Type-specific views by registering a view against a class.

Background

In 3: Traversal Hierarchies we had 3 "content types" (Root, Folder, and Document.) All, however, used the same view and template.

Pyramid traversal lets you bind a view to a particular content type. This ability to make your URLs "object oriented" is one of the distinguishing features of traversal, and makes crafting a URL space more natural. Once Pyramid finds the context object in the URL path, developers have a lot of flexibility in view predicates.

Objectives

  • Use a decorator @view_config which uses the context attribute to associate a particular view with context instances of a particular class.
  • Create views and templates which are unique to a particular class (a.k.a., type).
  • Learn patterns in test writing to handle multiple kinds of contexts.

Steps

  1. We are going to use the previous step as our starting point:

    $ cd ..; cp -r hierarchy typeviews; cd typeviews
    $ $VENV/bin/python setup.py develop
    
  2. Our views in typeviews/tutorial/views.py need type-specific registrations:

     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
    from pyramid.location import lineage
    from pyramid.view import view_config
    
    from .resources import (
        Root,
        Folder,
        Document
        )
    
    
    class TutorialViews:
        def __init__(self, context, request):
            self.context = context
            self.request = request
            self.parents = reversed(list(lineage(context)))
    
        @view_config(renderer='templates/root.jinja2',
                     context=Root)
        def root(self):
            page_title = 'Quick Tutorial: Root'
            return dict(page_title=page_title)
    
        @view_config(renderer='templates/folder.jinja2',
                     context=Folder)
        def folder(self):
            page_title = 'Quick Tutorial: Folder'
            return dict(page_title=page_title)
    
    
        @view_config(renderer='templates/document.jinja2',
                     context=Document)
        def document(self):
            page_title = 'Quick Tutorial: Document'
            return dict(page_title=page_title)
    
  3. We have a new contents subtemplate at typeviews/tutorial/templates/contents.jinja2:

    1
    2
    3
    4
    5
    6
    7
    8
    <h4>Contents</h4>
    <ul>
        {% for child in context.values() %}
            <li>
                <a href="{{ request.resource_url(child) }}">{{ child.title }}</a>
            </li>
        {% endfor %}
    </ul>
    
  4. Make a template for viewing the root at typeviews/tutorial/templates/root.jinja2:

    1
    2
    3
    4
    5
    6
    7
    8
    {% extends "templates/layout.jinja2" %}
    {% block content %}
    
        <h2>{{ context.title }}</h2>
        <p>The root might have some other text.</p>
        {% include "templates/contents.jinja2" %}
    
    {% endblock content %}
    
  5. Now make a template for viewing folders at typeviews/tutorial/templates/folder.jinja2:

    1
    2
    3
    4
    5
    6
    7
    {% extends "templates/layout.jinja2" %}
    {% block content %}
    
        <h2>{{ context.title }}</h2>
        {% include "templates/contents.jinja2" %}
    
    {% endblock content %}
    
  6. Finally make a template for viewing documents at typeviews/tutorial/templates/document.jinja2:

    1
    2
    3
    4
    5
    6
    7
    {% extends "templates/layout.jinja2" %}
    {% block content %}
    
        <h2>{{ context.title }}</h2>
        <p>A document might have some body text.</p>
    
    {% endblock content %}
    
  7. More tests are needed in typeviews/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
    51
    52
    53
    import unittest
    
    from pyramid.testing import DummyRequest
    from pyramid.testing import DummyResource
    
    
    class TutorialViewsUnitTests(unittest.TestCase):
        def _makeOne(self, context, request):
            from .views import TutorialViews
    
            inst = TutorialViews(context, request)
            return inst
    
        def test_site(self):
            request = DummyRequest()
            context = DummyResource()
            inst = self._makeOne(context, request)
            result = inst.root()
            self.assertIn('Root', result['page_title'])
    
        def test_folder_view(self):
            request = DummyRequest()
            context = DummyResource()
            inst = self._makeOne(context, request)
            result = inst.folder()
            self.assertIn('Folder', result['page_title'])
    
        def test_document_view(self):
            request = DummyRequest()
            context = DummyResource()
            inst = self._makeOne(context, request)
            result = inst.document()
            self.assertIn('Document', result['page_title'])
    
    
    class TutorialFunctionalTests(unittest.TestCase):
        def setUp(self):
            from tutorial import main
            app = main({})
            from webtest import TestApp
            self.testapp = TestApp(app)
    
        def test_it(self):
            res = self.testapp.get('/', status=200)
            self.assertIn(b'Root', res.body)
            res = self.testapp.get('/folder1', status=200)
            self.assertIn(b'Folder', res.body)
            res = self.testapp.get('/doc1', status=200)
            self.assertIn(b'Document', res.body)
            res = self.testapp.get('/doc2', status=200)
            self.assertIn(b'Document', res.body)
            res = self.testapp.get('/folder1/doc1', status=200)
            self.assertIn(b'Document', res.body)
    
  8. $ $VENV/bin/nosetests should report running 4 tests.

  9. Run your Pyramid application with:

    $ $VENV/bin/pserve development.ini --reload
    
  10. Open http://localhost:6543/ in your browser.

Analysis

For the most significant change, our @view_config now matches on a context view predicate. We can say "use this view when looking at this kind of thing." The concept of a route as an intermediary step between URLs and views has been eliminated.

Extra Credit

  1. Should you calculate the list of children on the Python side, or access it on the template side by operating on the context?
  2. What if you need different traversal policies?
  3. In Zope, interfaces were used to register a view. How do you register a Pyramid view against instances that support a particular interface? When should you?
  4. Let's say you need a more specific view to be used on a particular instance of a class, letting a more general view cover all other instances. What are some of your options?