DOMTemplate solves the problem of separating logic from presentation, and allows the template manipulation logic to be expressed in Python code form using the DOM API. However, the DOM API is too low level and it quickly becomes tedious to use to build complicated HTML structures.
Twisted's solution is to provide a Model-View-Controller based
component framework, which allows you to construct complex HTML
Views
out of many small interacting components, or
woven.widgets
.
Instead of manipulating DOM objects which represent low-level HTML
Nodes, you construct and compose the model data that your page will
be based on, and specify views which will be responsible for
formatting the model data as HTML. Using widgets defined in
twisted.web.woven.widgets, and higher-level widgets that you define
yourself for an application-specific purpose, python data structures
such as strings, integers, lists, dicts, and custom subclasses of
woven.model.Model
, can be adapted
implicitly or explicitly to subclasses of
woven.view.View
(such as subclasses
of woven.widgets.Widget
) for
display in HTML.
Model View Controller is a development strategy which
involves breaking up program logic into three separate domains:
Model objects, whose job it is to contain/produce data; View objects, whose
job it is to present this data to the user; and Controller objects,
whose job it is to handle events such as user input from a form
and
fetch URL
and update the model with the user's desired changes. When the
controller finishes updating the model, it tells the model to notify all views
that the model has changed so they may rerender themselves accordingly.
Woven's implementation of MVC uses
twisted.python.components
,
the interface and component registry, to loosely couple the interacting objects.
Let's start with the canonical Hello World example. We will use an instance
of woven.page.Page
as our
IResource
implementor.
IResource
describes the
interface that objects are required to implement in order to publish themselves
over the web in twisted.web. We will be instanciating a
Page
instance in an .rpy
script. An rpy is like a CGI script -- each time you visit the script, it is
executed. However, an rpy script is merely responsible for instanciating a
Resource
object to handle
the request and assigning it to a variable named
resource
. This
Resource
object will then
be called upon to render the request.
Next, let's take a look at the HTML template woven will look up to render this
request into HTML. Woven defines three special attributes, model=
,
view=
, and controller=
, which it uses to decide which
python code to invoke while rendering the page.
In the template, we have simply indicated that woven should replace certain
nodes with the results of rendering a widget on the current model. The syntax
model="."
indicates that woven should use the current model no
matter what its
name, similar to filesystem syntax. Since we aren't explicitly stating
which view widget should render the model with a view=
attribute, an
IView
adapter is
looked up from the global registry implicitly. In this case,
the model is a string, so an instance of
widgets.Text
is constructed
which converts the python string into a DOM text node and inserts it into the
DOM.
Next, let's look at an example of rendering a page with a more complicated
model. We're going to make several pieces of data available to the template
under different names. We will refer to these pieces of data as
Submodels
, since they are contained in a
Model
instance.
Listing 3: HelloWorld2.rpy: Setting up submodels with a resource script
We need a place to gather all the Model data together so the View has access to
it. woven.model.AttributeModel
is a
good container to place other models in,
and to do so we simply call setSubmodel. Notice that setSubmodel takes a key
and a value, the name the submodel will be available as to the template, and
the actual submodel data.
Woven comes with various widgets which are registered as
IView
implementors
for
the basic python types (strings, lists, and dictionaries) which are very
useful. Most of the time, you can simply prepare the data for rendering by
converting it into strings and lists using an rpy or custom Model subclass, and
then referencing these strings and lists in your template.
This time, in our HTML template, we're going to have to be a little more explicit when specifying view widgets to render the model data. It's generally a good idea to always explicitly state the name of the view widget you want to handle a node; but it's convenient that you don't have to, for example if you're rendering a custom model/view pair where it doesn't make sense to use any other view widgets to render a given model instance.
Listing 4: HelloWorld2.html: Explicitly stating view widget names in the template
As you can see from the template, the
List
widget requires
specially tagged
nodes inside of it's node in order to operate properly. These nodes are called
pattern
nodes, and each widget can choose to require certain patterns, or
look for certain optional patterns, during the course of rendering itself. In
the list widget's case, it looks for the pattern listItem
and makes one
copy of it for each element in the list it is rendering. There are two ways to
specify a patten node:
pattern="patternName"
attribute on a nodepatternName + 'Of'="modelName"
attribute on a
node
Look at the documentation for each individual widget to see what patterns a
Widget
supports. The
List
widget is
particularly useful; it supports the following patterns:
Notice a few things about this template. First, we are explicitly stating the
view widget we wish to render each node with a view=
attribute.
All these view widgets are defined in woven/widgets.py
, the
default woven widgets library. You can also create your own widget libraries
for your views, as well as defining subwidget names on more complicated views
that are only valid within that views's HTML node. Procedures for doing so will
be described in later HOWTOs.
You now know how to create a woven HTML template, and how to populate this
template with data in the form of simple python data types. However, often you
will wish to render a dynamic data source, such as a database, or a complex
data source such as a python object. One way to render this data over the web
is to create a class which implements
IModel
, the interface
woven uses to expose data to view widgets.
The IModel
interface is
documented in twisted.web.woven.interfaces
. It
describes the interfaces Models must implement in order to play well with the
rest of the woven MVC framework. If you are inheriting from
twisted.web.woven.model.Model
, most of these
interfaces will be implemented for you. The interfaces that we will be most
interested in implementing are those that are designed to be overridden for
customization,
getData
and
setData
.
Listing 5: wovenquotes.py: Implementing IModel to provide custom Model behavior
We have created a simple Model which wraps a quoter that was created in a previous HOWTO. The constructor stores the filename and creates a new FortuneQuoter instance.
Implementing
getData
is as simple as delegating to our FortuneQuoter instance.
getQuote
returns a string. In the template, we will specify that
the Text
widget should render the data returned by the quote model, so the quote shows
up in the template. But first, we need to get an instance of MQuote into the
model namespace, using an rpy:
Listing 6: wovenquotes.rpy: Tying together a custom Model subclass and an html template with an rpy
This time, instead of using an instance of AttributeModel as our main model
namespace, we have chosen to simply use a dictionary. Since a dictionary
doesn't implement
IModel
, an
attribute lookup occurs which wraps the dictionary in an instance of
DictionaryModel
, which
does implement
IModel
. Then all
of the dictionary keys will be available as submodel names.
Listing 7: WovenQuotes.xhtml: WovenQuotes Template
Woven templates are designed to minimize the amount of logic contained in the
HTML template. A woven template is a collection of HTML nodes that are tagged
with various strings which woven uses to locate python components that are then
responsible for producing the final output. In this case, we joined the output
of our Model
subclass'
getData
(a
string) with the Text
widget (which knows how to render strings into DOM).
The same theory is used for input handling by
Controllers
. In
the template, we
have placed the following input node:
<input type="text" name="quote" model="quote" controller="Anything" />
When woven encounters this node, it will look up the submodel name
quote
, and will get an instance of MQuote. It will then look up the
controller name
.
Anything
Anything
is an
InputHandler
(a
specialized type of controller
designed to handle web request input) defined in
twisted.web.woven.input
.
Similarly to woven.widgets
,
woven.input
is used as a default
controller namespace when searching for controller names.
The Anything
InputHandler
looks in
the request arguments for data with the same
name as its model name. In this case, the model name is quote
, and the
input node also has the name quote
. When the form is submitted, the
value of the text field will be available in the request arguments as
quote
, and the
Anything
InputHandler
will find
it. The
Anything
InputHandler
then
immediately
calls model.setData(value)
since it does no input validation.
Under construction.
Next up: Implementing custom view logic with wvupdate, and creating and using
reusable Widget
and
InputHandler
subclasses
with wvfactory and wcfactory.