summaryrefslogtreecommitdiff
path: root/docs/tutorials/lxmlgraph/step02.rst
blob: 1dc1ebcd3d56be81674535e0e60f692a63b95893 (plain)
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
================================================
Step 2: Hello World as XML
================================================

We now have a project named ``lxmlgraph``.  It contains a *package*
(also) named ``lxmlgraph``.

In this step we will add an XML document to the *package* as our model
data.  We will leverage the following :mod:`repoze.bfg` machinery:

  - Model data with interfaces that define "types"

  - ZCML configuration to provide type-specific views

Our application will need to do these things:

  - Use :term:`lxml` Element classes to inject :mod:`repoze.bfg`
    behavior into ``lxml`` nodes

  - That model class needs to implement the :mod:`repoze.bfg`
    publishing contract

All of the below filenames are relative to the ``lxmlgraph`` *package*
rather than the *project*.

``samplemodel.xml``
-----------------------------------

We're going to add an XML document that will serve as a source for
model data named ``samplemodel.xml``.  Put the content of this file in
your package:

.. literalinclude:: step02/myapp/samplemodel.xml
   :linenos:
   :language: xml

#. Line 2 provides the root of the model as an XML ``<site>`` node.
   The element name doesn't have to be ``<site>``.  It has a name of
   ``site``.

#. In lines 3-4, the ``<site>`` contains 2 top-level children: a and
   b.  These are provided as an element name ``<document>``.  This,
   also, is meaningless as far as :mod:`repoze.bfg` is concerned.
   However, this is where you compose the information model you are
   publishing.

The only special constraint is that an XML node that wants to be
"found" by :mod:`repoze.bfg` in during traversal *must* have a
``name`` attribute.  (The use of ``@name`` corresponds to ``__name__``
in the :mod:`repoze.bfg` sense of :term:`location` ).  Each hop in the
URL tries to grab a child with an attribute matching the next hop.
Also, the value of the ``@name`` should be unique in its containing
node.

Module ``models.py``
------------------------------

At a high level, we make write a class that "extends" ``lxml``
``Element`` nodes, create an ``lxml`` parser, and register the custom
class with the parser.  Replace the contents of the autogenerated
``models.py`` with the content we show below.

.. literalinclude:: step02/myapp/models.py
   :linenos:

#. Line 4 imports :term:`lxml`.

#. Line 9 creates the custom class we are going to use to extend
   etree.ElementBase.  The `<lxml website
   <http://codespeak.net/lxml/>`_ has great documentation on the
   various ways to inject custom Python behavior into XML.

#. Just as before, line 12 says that instances of this class support a
   certain content type (interface.)  In our case, instances will be
   XML nodes.

#. :mod:`repoze.bfg` has a protocol where model data should have an
   ``__name__`` attribute.  Lines 14-16 implement this by grabbing the
   ``@name`` attribute of the current node.

#. URL traversal in :mod:`repoze.bfg` works via the ``__getitem__``
   protocol.  Thus, we need a method that implements this.  Lines
   18-26 use XPath to look for a direct child that has an ``@name``
   matching the item name that's being traversed to.  If it finds it,
   return it.  If not, or if more than one is found, raise an error.

#. As before, ``get_root`` is the function that is expected to return
   the top of the model.  In lines 30+ we do the :term:`lxml` magic to
   get the custom Python class registered.  We then load some XML and
   return the top of the tree.

Module ``views.py``
-----------------------------

Replace the autogenerated ``views.py`` code in the ``lxmlgraph``
package with the following:

.. literalinclude:: step02/myapp/views.py
   :linenos:

Here's what that file does:

#. Line 5 grabs the element name (tag name) of the ``context``, which
   is the current XML node that we're traversing through.

#. Line 6 uses the special property we defined in our custom Python
   class to get the ``__name__`` of the context.

We don't need to change the ``configure.zcml`` because the
autogenerated one is still correct for this configuration.  It
includes:

.. literalinclude:: step02/myapp/configure.zcml
   :linenos:
   :language: xml

Browsing the Model
------------------------

We're done changing code.  Start the application by executing ``paster
serve lxmlgraph.ini`` (the ``.ini`` file is in the project directory).
It will listen on port 5432.  We can use these URLs to browse the
model graph and see results::

  http://localhost:5432/a (Hello to document from a @ /a)

  http://localhost:5432/b (Hello to document from b @ /b)

  http://localhost:5432/c (Not Found)

In this case, each request grabs a node in the XML and uses it as the
data for the view.