Views¶
Introduction¶
Morepath views are looked up through the URL path, but not through the routing procedure. Routing stops at models. Then the last segment of the path is taken to identify the view by name.
Named views¶
Let’s examine a path:
/documents/1/edit
If there’s a model like this:
@App.path(model=Document, path='/documents/{id}')
def get_document(id):
return query_for_document(id)
then /edit
identifies a view named edit
on the Document
model (or
on one of its base classes). Here’s how we define it:
@App.view(model=Document, name='edit')
def document_edit(self, request):
return "edit view on model: %s" % self.id
Default views¶
Let’s examine this path:
/documents/1
If the model is published on the path /documents/{id}
, then this is
a path to the default view of the model. Here’s how that view is
defined:
@App.view(model=Document)
def document_default(self, request):
return "default view on model: %s" % self.id
The default view is the view that gets triggered if there is no
special path segment in the URL that indicates a specific view. The
default view has as its name the empty string ""
, so this
registration is the equivalent of the one above:
@App.view(model=Document, name="")
def document_default(self, request):
return "default view on model: %s" % self.id
Generic views¶
Generic views in Morepath are nothing special: the thing that makes them generic is that their model is a base class, and inheritance does the rest. Let’s see how that works.
What if we want to have a view that works for any model that
implements a certain API? Let’s imagine we have a Collection
model:
class Collection(object):
def __init__(self, offset, limit):
self.offset = offset
self.limit = limit
def query(self):
raise NotImplementedError
A Collection
represents a collection of objects, which can be
ordered somehow. We restrict the objects we actually get by offset and
limit. With offset 100 and limit 10, we get objects 100 through 109.
Collection
is a base class, so we don’t actually implement how to
do a query. That’s up to the subclasses. We do specify that query is
supposed to return objects that have an id
attribute.
We can create a view to this abstract collection that displays the ids of the things in it in a comma separated list:
@App.view(model=Collection)
def collection_default(self, request):
return ", ".join([str(item.id) for item in self.query()])
This view is generic: it works for any kind of collection.
We can now create a concrete collection that fulfills the requirements:
class Item(object):
def __init__(self, id):
self.id = id
class MyCollection(Collection):
def query(self):
return [Item(str(i)) for i in
range(self.offset, self.offset + self.limit)
When we now publish the concrete MyCollection
on some URL:
@App.path(model=MyCollection, path='my_collection')
def get_my_collection():
return MyCollection()
it automatically gains a default view for it that represents the ids
in it as a comma separated list. So the view collection_default
is
generic.
Details¶
The decorator morepath.App.view()
(@App.view
) takes two
arguments here, model
, which is the class of the model the view is
representing, and name
, which is the name of the view in the URL
path.
The @App.view
decorator decorates a function that takes two arguments:
a self
and a request
.
The self
object is the model that’s being viewed, i.e. the one
found by the get_document
function. It is going to be an instance
of the class given by the model
parameter.
The request
object is an instance of morepath.Request
,
which in turn is a special kind of
webob.request.BaseRequest
. You can get request information
from it like arguments or form data, and it also exposes a few special
methods, such as morepath.Request.link()
.
The @App.path
and @App.view
decorators are associated by
indirectly their model
parameters: the view works for a given
model path if the model
parameter is the same, or if the view is
associated with a base class of the model exposed by the
@App.path
decorator.
Ambiguity between path and view¶
Let’s examine these simple paths in an application:
/folder
/folder/{name}
/folder
shows an overview of the items in it. /folder/{name}
is a way to get to an individual item.
This means:
/folder/some_item
is a path if there is an item in the folder with the name
some_item
.
Now what if we also want to have a path that allows you to edit the folder? It’d be natural to spell it like this:
/folder/edit
i.e. there is a path /folder
with a view edit
.
But now we have a problem: how does Morepath know that edit
is a
view and not a named item in the folder? The answer is that it
doesn’t. You cannot reach the view this way.
Instead we have to make it explicit in the path that we want a view with
a +
character:
/folder/+edit
Now Morepath won’t try to interpret +edit
as a named item in the
folder, but instead looks up the view.
Any view can be addressed not just by name but also by its name with a
+
prefix. To generate a link to a name with a +
prefix you can
use the prefix as well, so you can write:
request.link(my_folder, '+edit')
render¶
By default @App.view
returns either a morepath.Response
object or a string that gets turned into a response. The
content-type
of the response is not set. For a HTML response you
want a view that sets the content-type
to text/html
. You can
do this by passing a render
parameter to the @App.view
decorator:
@App.view(class=Document, render=morepath.render_html)
def document_default(self, request):
return "<p>Some html</p>"
morepath.render_html()
is a very simple function:
def render_html(content, request):
response = morepath.Response(content)
response.content_type = 'text/html'
return response
You can define your own render
functions; they just need to take
some content (any object, in this case its a string), and return a
Response
object.
Another render function is morepath.render_json()
. Here it is:
import json
def render_json(content, request):
response = morepath.Response(json.dumps(content))
response.content_type = 'application/json'
return response
We’d use it like this:
@App.view(class=Document, render=morepath.render_json)
def document_default(self, request):
return {'my': 'json'}
HTML views and JSON views are so common we have special shortcut decorators:
@App.html
(morepath.App.html()
)@App.json
(morepath.App.json()
)
Here’s how you use them:
@App.html(class=Document)
def document_default(self, request):
return "<p>Some html</p>"
@App.json(class=Document)
def document_default(self, request):
return {'my': 'json'}
Templates¶
You can use a server template with a view by using the template
argument:
@App.html(model=Document, template='document.pt')
def document_default(self, request):
return { 'title': self.title, 'content': self.content }
See Templates for more information.
Permissions¶
We can protect a view using a permission
. A permission is any
Python class:
class Edit(object):
pass
The class doesn’t do anything; it’s just a marker for permission.
You can use such a class with a view:
@App.view(model=Document, name='edit', permission=Edit)
def document_edit(self, request):
return 'edit document'
You can define which users have what permission on which models by using
the morepath.App.permission_rule()
decorator. To learn more,
read Security.
Manipulating the response¶
Sometimes you want to do things to the response specific to the view,
so that you cannot do it in a render
function. Let’s say you want
to add a cookie using webob.Response.set_cookie()
. You don’t
have access to the response object in the view, as it has not been
created yet. It is only created after the view has returned. We can
register a callback function to be called after the view is done and
the response is ready using the morepath.Request.after()
decorator. Here’s how:
@App.view(model=Document)
def document_default(self, request):
@request.after
def manipulate_response(response):
response.set_cookie('my_cookie', 'cookie_data')
return "document default"
after
only applies if the view was successfully resolved into a
response. If your view raises an exception for any reason, or if
Morepath itself does, any after
set in the view does not apply to
the response for this exception. If the view returns a response
object directly itself, then after
is also not run - you have the
response object to manipulate directly. Note that this the case when
you use morepath.redirect()
: this returns a redirect response
object.
request_method¶
By default, a view only answers to a GET
request: it doesn’t
handle other request methods like POST
or PUT
or DELETE
. To
write a view that handles another request method you need to be explicit and
pass in the request_method
parameter:
@App.view(model=Document, name='edit', request_method='POST')
def document_edit(self, request):
return "edit view on model: %s" % self.id
Now we have a view that handles POST
. Normally you cannot have
multiple views for the same document with the same name: the Morepath
configuration engine rejects that. But you can if you make sure they
each have a different request method:
@App.view(model=Document, name='edit', request_method='GET')
def document_edit_get(self, request):
return "get edit view on model: %s" % self.id
@App.view(model=Document, name='edit', request_method='POST')
def document_edit_post(self, request):
return "post edit view on model: %s" % self.id
Grouping views¶
At some point you may have a lot of view decorators that share a lot of information; multiple views for the same model are the most common example.
Instead of writing this:
@App.view(model=Document)
def document_default(self, request):
return "default"
@App.view(model=Document, name='edit')
def document_edit(self, request):
return "edit"
You can use the with
statement to write this instead:
with App.view(model=Document) as view:
@view()
def document_default(self, request):
return "default"
@view(name="edit")
def document_edit(self, request):
return "edit"
This is equivalent to the above, you just don’t have to repeat
model=Document
. You can use this for any parameter for
@App.view
.
This use of the with
statement is in fact general; it can be used
like this with any Morepath directive, and with any parameter for such
a directive. The with
statement may even be nested, though we
recommend being careful with that, as it introduces a lot of
indentation.
Predicates¶
The model
, name
, request_method
and body_model
arguments on the @App.view
decorator are examples of view
predicates. You can add new ones by using the
morepath.App.predicate()
decorator.
Let’s say we have a view that we only want to kick in when a certain request header is set to something:
import reg
@App.predicate(generic.view, name='something', default=None,
index=reg.KeyIndex,
after=morepath.LAST_VIEW_PREDICATE)
def something_predicate(request):
return request.headers.get('Something')
We can use any information in the request and model to construct the
predicate. Now you can use it to make a view that only kicks in when
the Something
header is special
:
@App.view(model=Document, something='special')
def document_default(self, request):
return "Only if request header Something is set to special."
If you have a predicate and you don’t use it in a @App.view
, or
set it to None
, the view works for the default
value for that
predicate. The default
parameter is also used when rendering a
view using morepath.Request.view()
and you don’t pass in a
particular value for that predicate.
Let’s look into the predicate directive in a bit more detail.
You can use either self
or request
as the argument for the
predicate function. Morepath sees this argument and sends in either
the object instance or the request.
We use reg.KeyIndex
as the index for this predicate. You can also
have predicate functions that return a Python class. In that case you
should use reg.ClassIndex
.
morepath.LAST_VIEW_PREDICATE
is the last predicate defined by Morepath
itself. Here we want to insert the something_predicate
after this
predicate in the predicate evaluation order.
The after
parameter for the predicate determines which predicates
match more strongly than another; a predicate after another one
matches more weakly. If there are two view candidates that both match
the predicates, the strongest match is picked.
request.view¶
It is often useful to be able to compose a view from other
views. Let’s look at our earlier Collection
example again. What if
we wanted a generic view for our collection that included the views
for its content? This is easiest demonstrated using a JSON view:
@App.json(model=Collection)
def collection_default(self, request):
return [request.view(item) for item in self.query()]
Here we have a view that for all items returned by query includes its
view in the resulting list. Since this view is generic, we cannot
refer to a specific view function here; we just want to use the
view function appropriate to whatever item
may be. For this
we can use morepath.Request.view()
.
We could for instance have a particular item with a view like this:
@App.json(model=ParticularItem)
def particular_item_default(self, request):
return {'id': self.id}
And then the result of collection_default
is something like:
[{'id': 1}, {'id': 2}]
but if we have a some other item with a view like this:
@App.json(model=SomeOtherItem)
def some_other_item_default(self, request):
return self.name
where the name is some string like alpha
or beta
, then the
output of collection_default
is something like:
['alpha', 'beta']
So request.view
can make it much easier to construct composed JSON
results where JSON representations are only loosely coupled.
You can also use predicates
in request.view
. Here we get the
view with the name
"edit"
and the request_method
"POST"
:
request.view(item, name="edit", request_method="POST")
You can also create views that are for internal use only. You can use
them with request.view()
but they won’t show up to the web; going
to such a view is a 404 error. You can do this by passing the internal
flag to the directive:
@App.json(model=SomeOtherItem, name='extra', internal=True)
def some_other_item_extra(self, request):
return self.name
The extra
view can be used with request.view(item,
name='extra')
, but it is not available on the web – there is no
/extra
view.
Exception views¶
Sometimes your application raises an exception. This can either be a HTTP exception, for instance when the user goes to a URL that does not exist, or an arbitrary exception raised by the application.
HTTP exceptions are by default rendered in the standard WebOb way, which includes some text to describe Not Found, etc. Other exceptions are normally caught by the web server and result in a HTTP 500 error (internal server error).
You may instead want to customize what these exceptions look like. You can do so by declaring a view using the exception class as the model. Here’s how you make a custom 404 Not Found:
from webob.exc import HTTPNotFound
@App.view(model=HTTPNotFound)
def notfound_custom(self, request):
def set_status_code(response):
response.status_code = self.code # pass along 404
request.after(set_status_code)
return "My custom not found!"
We have to add the set_status_code
to make sure the response is
still a 404; otherwise we change the 404 to a 200 Ok! This shows that
self
is indeed an instance of HTTPNotFound
and we can access
its code
attribute.
Your application may also define its own custom exceptions that have a meaning particular to the application. You can create custom views for those as well:
class MyException(Exception):
pass
@App.view(model=MyException)
def myexception_default(self, request):
return "My exception"
Without an exception view for MyException
any view code that raises
MyException
would bubble all the way up to the WSGI server and
a 500 Internal Server Error is generated.
But with the view for MyException
in place, whenever
MyException
is raised you get the special view instead.